diff --git a/bigbluebutton-client/branding/default/style/css/V2Theme.css b/bigbluebutton-client/branding/default/style/css/V2Theme.css
index 7130ffb6952d999bdd5f17701629942ff27d3f8b..99e8815333120669ee0e9323368779531d3bc537 100755
--- a/bigbluebutton-client/branding/default/style/css/V2Theme.css
+++ b/bigbluebutton-client/branding/default/style/css/V2Theme.css
@@ -645,12 +645,25 @@ chat|AddChatTabBox {
 	fontSize : 14;
 }
 
+chat|ChatMessageRenderer {
+	moderatorIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_User_Chat_Moderator");
+}
+
 .chatMessageListStyle {
 	borderStyle    : none;
 	rollOverColor  : #FFFFFF;
 	selectionColor : #CED4DB;
 }
 
+.chatMessageHeader {
+	color : #808080;
+}
+
+.chatMessageHeaderModerator {
+	color      : #808080;
+	fontWeight : bold;
+}
+
 /*
 //------------------------------
 //  CheckBox
@@ -876,6 +889,7 @@ layout|LoadButton {
 
 views|LogoutWindow, views|LoggedOutWindow {
 	headerHeight              : 32;
+	horizontalAlign           : center;
 	paddingBottom             : 8;
 	paddingLeft               : 8;
 	paddingRight              : 8;
@@ -892,6 +906,58 @@ views|LogoutWindow, views|LoggedOutWindow {
 	paddingBottom : 8;
 }
 
+.logoutRatingBox {
+	paddingTop      : 12;
+	paddingBottom   : 12;
+	horizontalAlign : center;
+	verticalGap     : 8;
+}
+
+.logoutTitle {
+	fontSize : 24;
+}
+
+.logoutSubTitle {
+	fontSize : 16;
+	color    : #4E5A66;
+}
+
+.loggedOutContainer {
+	horizontalAlign : center;
+	verticalGap     : 4;
+}
+
+.logoutFeedbackHintBoxStyle {
+	backgroundColor : #CDD4DB;
+	paddingLeft     : 0;
+	paddingRight    : 0;
+	verticalGap     : 0;
+}
+
+views|StarRating {
+	paddingTop    : 4;
+	paddingBottom : 4;
+	verticalGap   : 8;
+	horizontalGap : 0;
+	emptyStar     : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Star_Empty");
+	filledStar    : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Star_Filled");
+}
+
+.starBoxStyle {
+	horizontalAlign : center;
+	verticalGap     : 4;
+	paddingLeft     : 0;
+	paddingRight    : 0;
+	paddingTop      : 4;
+	paddingBottom   : 4;
+
+}
+
+.logoutRule {
+	strokeWidth : 1;
+	color       : #BBBDBF;
+}
+
 /*
 //------------------------------
 //  Lock Settings
@@ -1320,7 +1386,7 @@ mx|Panel {
 	paddingRight    : 10;
 }
 
-.presentationUploadFileFormatHintTextStyle, .audioBroswerHintTextStyle {
+.presentationUploadFileFormatHintTextStyle, .audioBroswerHintTextStyle, .logoutFeedbackHint {
 	fontWeight : bold;
 }
 
@@ -1730,7 +1796,6 @@ videoconf|UserGraphicHolder {
 
 .userGraphicBackground {
 	backgroundColor : #FFFFFF;
-	borderStyle     : solid;
 	borderColor     : #000000;
 	borderThickness : 0;
 }
diff --git a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla
index 1459fd1e63cfab534cfd2b55158033d256fde30c..cbb251fff010ab63527aec50062f1b17dde243ed 100644
Binary files a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla and b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla differ
diff --git a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf
index 8f58cde856c5f2c40b946bd3f7076602a950d53b..782dcfd94ff90ffe34ded77a5e3c87bae8d750e3 100644
Binary files a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf and b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf differ
diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties
index cfe4a9a17260cd067af93e04fc2a5bcf3a971d9d..38ac43086c5ddcc69f0808bf628cd2b0360328aa 100755
--- a/bigbluebutton-client/locale/en_US/bbbResources.properties
+++ b/bigbluebutton-client/locale/en_US/bbbResources.properties
@@ -501,7 +501,6 @@ bbb.highlighter.toolbar.color.accessibilityName = Whiteboard mark draw color
 bbb.highlighter.toolbar.thickness = Change Thickness
 bbb.highlighter.toolbar.thickness.accessibilityName = Whiteboard draw thickness
 bbb.highlighter.toolbar.multiuser = Multi-user Drawing
-bbb.logout.title = Logged Out
 bbb.logout.button.label = OK
 bbb.logout.appshutdown = The server app has been shut down
 bbb.logout.asyncerror = An Async Error occured
@@ -516,6 +515,8 @@ bbb.logour.breakoutRoomClose = Your browser window will be closed
 bbb.logout.ejectedFromMeeting = You have been removed from the meeting.
 bbb.logout.refresh.message = If this logout was unexpected click the button below to reconnect.
 bbb.logout.refresh.label = Reconnect
+bbb.logout.feedback.hint = How can we make BigBlueButton better?
+bbb.logout.feedback.label = We'd love to hear about your experience with BigBlueButton (optional)
 bbb.settings.title = Settings
 bbb.settings.ok = OK
 bbb.settings.cancel = Cancel
diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template
index d9b3d99a38f6ff24cc2b828df14530e64ed05ac3..7be9c348757467430ed9bfb942dc2ed8d85c8d90 100755
--- a/bigbluebutton-client/resources/config.xml.template
+++ b/bigbluebutton-client/resources/config.xml.template
@@ -15,7 +15,8 @@
     <layout showLogButton="false" defaultLayout="bbb.layout.name.defaultlayout"
             showToolbar="true" showFooter="true" showMeetingName="true" showHelpButton="true"
             showLogoutWindow="true" showLayoutTools="true" confirmLogout="true" showNetworkMonitor="false"
-            showRecordingNotification="true" logoutOnStopRecording="false"/>
+            showRecordingNotification="true" logoutOnStopRecording="false"
+            askForFeedbackOnLogout="false"/>
     <breakoutRooms enabled="true" record="false" privateChateEnabled="true"/>
     <logging enabled="true" logTarget="trace" level="info" format="{dateUTC} {time} :: {name} :: [{logLevel}] {message}" uri="http://HOST/log" logPattern=".*"/>
     <lock disableCam="false" disableMic="false" disablePrivateChat="false"
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
index 3aa4e9243994311f35964cb79379d1a53cc74f74..1086d53d8af9780648f1c519a45d8d8f7adc331c 100644
--- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js
+++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
@@ -420,6 +420,11 @@ window.getScreenConstraints = function(sendSource, callback) {
 
   if(isChrome) {
     getChromeScreenConstraints ((constraints) => {
+      if(!constraints){
+        document.dispatchEvent(new Event("installChromeExtension"));
+        return;
+      }
+      extensionInstalled = true;
       let sourceId = constraints.streamId;
 
       // this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as b/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
index a0285e0084a930b66c693bc68b1ae5668a41ba84..0aabefb112746b9797b3885ce482b7cddffd3d7d 100644
--- a/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
@@ -103,7 +103,7 @@ package org.bigbluebutton.core {
 		private static function addPopUpToStage(parent:DisplayObject, className:Class, modal:Boolean = false, center:Boolean = true):IFlexDisplayObject {
 			var popUp:IFlexDisplayObject = PopUpManager.createPopUp(parent, className, modal);
 			if (center) {
-				PopUpManager.centerPopUp(popUp)
+				PopUpManager.centerPopUp(popUp);
 			}
 			popUpDict[getQualifiedClassName(className)] = popUp;
 
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/options/LayoutOptions.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/options/LayoutOptions.as
index 858976ec3dda6ec16c25b2f8e51eb8de1aff09a7..4393f8fa313e68a26428e773915f60d8cd0e05cc 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/model/options/LayoutOptions.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/options/LayoutOptions.as
@@ -54,6 +54,9 @@ package org.bigbluebutton.main.model.options {
 		[Bindable]
 		public var showNetworkMonitor:Boolean = false;
 
+		[Bindable]
+		public var askForFeedbackOnLogout:Boolean = false;
+
 		public var defaultLayout:String = "Default";
 
 		public function LayoutOptions() {
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml
index 82d3ae20890b63350c07da17290c672329793505..411da10098ac0e7990dde4e8781a9e6b00dc3271 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml
@@ -20,27 +20,40 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 -->
 
-<mx:TitleWindow xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:fx="http://ns.adobe.com/mxml/2009"
-    title="{ResourceUtil.getInstance().getString('bbb.logout.title')}" showCloseButton="false" creationComplete="init()"
-    verticalScrollPolicy="off" horizontalScrollPolicy="off"
-    x="168" y="86" layout="vertical" width="400" horizontalAlign="center">
+<mx:TitleWindow xmlns:mx="library://ns.adobe.com/flex/mx"
+				xmlns:fx="http://ns.adobe.com/mxml/2009"
+				xmlns:views="org.bigbluebutton.main.views.*"
+				showCloseButton="false"
+				creationComplete="init()"
+				resize="resizeHandler(event)"
+				verticalScrollPolicy="off"
+				horizontalScrollPolicy="off"
+				layout="vertical"
+				y="86"
+				width.logout="400"
+				width.feedback="600">
 	<fx:Script>
 		<![CDATA[
 			import com.asfusion.mate.events.Dispatcher;
-			
+
 			import flash.net.URLLoader;
 			import flash.net.URLRequest;
 			import flash.net.URLRequestMethod;
-			
+
+			import mx.events.ResizeEvent;
+			import mx.managers.PopUpManager;
+
 			import org.as3commons.logging.api.ILogger;
 			import org.as3commons.logging.api.getClassLogger;
 			import org.bigbluebutton.core.BBB;
+			import org.bigbluebutton.core.Options;
 			import org.bigbluebutton.core.PopUpUtil;
 			import org.bigbluebutton.core.UsersUtil;
 			import org.bigbluebutton.main.events.ExitApplicationEvent;
+			import org.bigbluebutton.main.model.options.LayoutOptions;
 			import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent;
 			import org.bigbluebutton.util.i18n.ResourceUtil;
-			
+
 			private static const LOGGER:ILogger = getClassLogger(LoggedOutWindow);
 
 			[Bindable]
@@ -53,6 +66,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 
 			private function callSignOut():void {
+				if (currentState == "feedback" && starRating.rating > 0) {
+					var logData:Object = new Object();
+					logData.username = UsersUtil.getMyUsername();
+					logData.userId = UsersUtil.getMyUserID();
+					logData.meetingId = UsersUtil.getInternalMeetingID();
+					logData.rating = starRating.rating;
+					logData.comment = feedbackMsg.text;
+					LOGGER.info(JSON.stringify(logData));
+				}
+
 				var logoutURL:String = BBB.getSignoutURL();
 				var request:URLRequest = new URLRequest(logoutURL);
 				LOGGER.debug("Log out url: " + logoutURL);
@@ -95,6 +118,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				logData.reason = reason;
 				LOGGER.info(JSON.stringify(logData));
 
+				var layoutOptions:LayoutOptions = Options.getOptions(LayoutOptions) as LayoutOptions;
+				if (layoutOptions.askForFeedbackOnLogout) {
+					currentState = "feedback";
+				}
+
 				switch (reason) {
 					case ConnectionFailedEvent.APP_SHUTDOWN:
 						message = ResourceUtil.getInstance().getString('bbb.logout.appshutdown');
@@ -128,10 +156,56 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 					message += "\n" + ResourceUtil.getInstance().getString('bbb.logour.breakoutRoomClose');
 				}
 			}
+
+			protected function resizeHandler(event:ResizeEvent):void {
+				PopUpManager.centerPopUp(this);
+			}
 		]]>
 	</fx:Script>
-	<mx:VBox width="100%" height="100%" horizontalAlign="center">
-		<mx:Text text="{message}" textAlign="center"/>
-		<mx:Button id="okBtn" styleName="mainActionButton" label="{ResourceUtil.getInstance().getString('bbb.logout.button.label')}" click="callSignOut()"/>
+
+	<mx:states>
+		<mx:State name="logout" />
+		<mx:State name="feedback" />
+	</mx:states>
+	<mx:VBox width="100%"
+			 height="100%"
+			 styleName="loggedOutContainer"
+			 horizontalAlign="center">
+		<mx:Text text="{message}"
+				 styleName.logout="logoutTitleWindowStyle"
+				 styleName.feedback="logoutTitle" />
+		<mx:VBox id="titleBox"
+				 includeIn="feedback"
+				 styleName="logoutRatingBox">
+			<mx:HRule width="550"
+					  height="30"
+					  styleName="logoutRule" />
+			<mx:Label text="{ResourceUtil.getInstance().getString('bbb.logout.feedback.label')}"
+					  styleName="logoutSubTitle" />
+			<views:StarRating id="starRating" />
+			<mx:VBox width="350"
+					 verticalAlign="middle"
+					 horizontalAlign="center"
+					 visible="{starRating.rating > 0}"
+					 includeInLayout="{starRating.rating > 0}"
+					 styleName="logoutFeedbackHintBoxStyle">
+				<mx:Text width="100%"
+						 textAlign="center"
+						 selectable="false"
+						 text="{ResourceUtil.getInstance().getString('bbb.logout.feedback.hint')}"
+						 styleName="logoutFeedbackHint" />
+				<mx:TextArea id="feedbackMsg"
+							 maxChars="512"
+							 width="100%"
+							 height="90" />
+			</mx:VBox>
+			<mx:HRule width="550"
+					  height="30"
+					  styleName="logoutRule" />
+		</mx:VBox>
+		<mx:Button id="okBtn"
+				   styleName="mainActionButton"
+				   label="{ResourceUtil.getInstance().getString('bbb.logout.button.label')}"
+				   click="callSignOut()" />
 	</mx:VBox>
-</mx:TitleWindow> 
+</mx:TitleWindow>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
index 8a86e4a2de30ed45db304fab9e4bd9968d1acaf5..e0086ceef3bed9b7a295bcc11c4b6ffa33e215c0 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
@@ -25,7 +25,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		  implements="org.bigbluebutton.common.IKeyboardClose"
 		  verticalScrollPolicy="off"
           horizontalScrollPolicy="off"
-          horizontalAlign="center"
           showCloseButton="false"
           creationComplete="onCreationComplete()"
           minWidth="350"
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/StarRating.as b/bigbluebutton-client/src/org/bigbluebutton/main/views/StarRating.as
new file mode 100644
index 0000000000000000000000000000000000000000..eb55174fb4f517dce47de84f85f8fc0f1455513a
--- /dev/null
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/StarRating.as
@@ -0,0 +1,85 @@
+/**
+ * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+ *
+ * Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+ *
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation; either version 3.0 of the License, or (at your option) any later
+ * version.
+ *
+ * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.bigbluebutton.main.views {
+	import flash.events.MouseEvent;
+	
+	import mx.containers.HBox;
+	import mx.containers.VBox;
+	import mx.controls.Image;
+	import mx.core.UIComponent;
+	
+	import org.as3commons.lang.StringUtils;
+
+	public class StarRating extends HBox {
+
+		[Bindable]
+		public var rating:int = 0;
+
+		private function setRating(value:int):void {
+			rating = value;
+		}
+
+		override protected function createChildren():void {
+			super.createChildren();
+
+			for (var i:int = 1; i <= 5; i++) {
+				addStar(i);
+			}
+		}
+
+		private function addStar(index:int):void {
+			var starBox:VBox = new VBox();
+			starBox.id = "starBox" + index;
+			starBox.styleName = "starBoxStyle";
+			starBox.width = 50;
+			starBox.addEventListener(MouseEvent.MOUSE_OVER, starBoxMouseOverHandler);
+			starBox.addEventListener(MouseEvent.MOUSE_OUT, starBoxMouseOutHandler);
+			starBox.addEventListener(MouseEvent.CLICK, starBoxClickHandler);
+			var starImage:Image = new Image();
+			starImage.source = getStyle('emptyStar');
+			starBox.addChild(starImage);
+			this.addChild(starBox);
+		}
+
+		private function starBoxMouseOverHandler(event:MouseEvent):void {
+			fillStars(getCurrentBoxIndex(event.currentTarget as UIComponent));
+		}
+
+		private function starBoxMouseOutHandler(event:MouseEvent):void {
+			fillStars(rating);
+		}
+
+		private function fillStars(max:int):void {
+			for (var i:int = 1; i <= max; i++) {
+				Image(VBox(getChildAt(i - 1)).getChildAt(0)).source = getStyle('filledStar');
+			}
+			for (var j:int = max + 1; j <= numChildren; j++) {
+				Image(VBox(getChildAt(j - 1)).getChildAt(0)).source = getStyle('emptyStar');
+			}
+		}
+
+		private function starBoxClickHandler(event:MouseEvent):void {
+			setRating(getCurrentBoxIndex(event.currentTarget as UIComponent));
+		}
+
+		private function getCurrentBoxIndex(component:UIComponent):int {
+			return parseInt(StringUtils.remove(component.id, "starBox"));
+		}
+	}
+}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
index e0d4a88492213a237d49f7e0473a69a3f1648e37..982e0d5ed37572c9bce5b94031ff4608c5736ac9 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
@@ -41,7 +41,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	<fx:Script>
 		<![CDATA[
 			import com.asfusion.mate.events.Dispatcher;
-			
+
+			import flash.utils.setTimeout;
+
 			import org.as3commons.logging.api.ILogger;
 			import org.as3commons.logging.api.getClassLogger;
 			import org.bigbluebutton.core.Options;
@@ -54,6 +56,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			import org.bigbluebutton.modules.phone.events.WebRTCJoinedVoiceConferenceEvent;
 			import org.bigbluebutton.modules.phone.models.Constants;
 			import org.bigbluebutton.modules.phone.models.PhoneModel;
+			import org.bigbluebutton.util.browser.BrowserCheck;
 			import org.bigbluebutton.util.i18n.ResourceUtil;
 
 			private static const LOGGER:ILogger = getClassLogger(WebRTCEchoTest);      
@@ -169,9 +172,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 			
       private function webRTCEchoTestStarted():void {
-        setCurrentState("started");
-        stopTimers();
+		var timeOut : Number = 50;
+		if ( BrowserCheck.isFirefox() ) {
+			timeOut = 1000;
+		}
+		setTimeout(setStartedState, timeOut);
       }
+			
+	  private function setStartedState():void {
+		  setCurrentState("started");
+		  stopTimers();
+	  }
       
 			private function handleWebRTCEchoTestEndedEvent(e:WebRTCEchoTestEvent):void {
         webRTCEchoTestEnded();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml
index a9a8dac5ff266672d32f2e9e56c09e980cc087bf..b8ca99fb89258dcd98fed20957fc43ba1b40be4b 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml
@@ -33,16 +33,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			
 			import org.as3commons.logging.api.ILogger;
 			import org.as3commons.logging.api.getClassLogger;
+			import org.bigbluebutton.common.Role;
+			import org.bigbluebutton.core.UsersUtil;
 			
 			private static const LOGGER:ILogger = getClassLogger(ChatMessageRenderer);      
 
 			private function onLinkClick(e:TextEvent):void{
-        LOGGER.debug("Clicked on link[{0}] from chat", [e.text]);
-        if (ExternalInterface.available) {
-          ExternalInterface.call("chatLinkClicked", e.text);
-        } 
+				LOGGER.debug("Clicked on link[{0}] from chat", [e.text]);
+				if (ExternalInterface.available) {
+					ExternalInterface.call("chatLinkClicked", e.text);
+				}
 			}
-			
+
 			//private function dataChangeHandler(e:Event = null):void{
 			override public function set data(value:Object):void {
 				//if (data == null) {
@@ -63,6 +65,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				//remove the header if not needed to save space
 				hbHeader.includeInLayout = hbHeader.visible = lblName.visible || lblTime.visible;
 				
+				if (data.hasOwnProperty("senderId") && UsersUtil.getUser(data.senderId) && UsersUtil.getUser(data.senderId).role == Role.MODERATOR) {
+					hbHeader.styleName = "chatMessageHeaderModerator";
+					if (lblName.visible) {
+						moderatorIcon.visible = true;
+					}
+				}
+				
 				// If you remove this some of the chat messages will fail to render
 				validateNow();
 			}
@@ -76,11 +85,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		]]>
 	</fx:Script>
 	
-  <mx:Canvas width="100%" id="hbHeader" verticalScrollPolicy="off" horizontalScrollPolicy="off">
-    <mx:Label id="lblName" text="{data.name}" visible="true" color="gray" textAlign="left" left="0" width="{this.width - lblTime.width - 22}"/>
+  <mx:Canvas width="100%" id="hbHeader" styleName="chatMessageHeader" verticalScrollPolicy="off" horizontalScrollPolicy="off">
+    <mx:Label id="lblName" text="{data.name}" visible="true" verticalCenter="0" textAlign="left" left="0" maxWidth="{this.width - lblTime.width - moderatorIcon.width - 22}"/>
+	<mx:Image id="moderatorIcon" visible="false" source="{getStyle('moderatorIcon')}" x="{lblName.width + 4}" verticalCenter="0"/>
     <mx:Text id="lblTime" htmlText="{data.time}" textAlign="right"
-             visible="true" 
-             color="gray" right="4" />
+			 verticalCenter="0"
+             visible="true"
+             right="4" />
   </mx:Canvas>
   <mx:Text id="txtMessage" htmlText="{data.text}" link="onLinkClick(event)" color="{data.senderColor}"
            keyDown="onKeyDown(event)" paddingLeft="5" width="100%" selectable="true"/>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreenshareViewWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreenshareViewWindow.mxml
index 632d75c6e7ef34ed387a0b3cff748e820d3b954a..8f20b6cf25a9ac2a8f99b8fae7a18ffe46e4a81b 100644
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreenshareViewWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreenshareViewWindow.mxml
@@ -29,6 +29,8 @@
            initialize="init()"
 		   layout="absolute"
            creationComplete="onCreationComplete()"
+		   verticalScrollPolicy="off"
+		   horizontalScrollPolicy="off"
            implements="org.bigbluebutton.common.IBbbModuleWindow"
            xmlns:mate="http://mate.asfusion.com/"
            title="{    ResourceUtil.getInstance().getString('bbb.screenshareView.title')    }"
@@ -125,7 +127,7 @@
 			}
 			
 			private function videoHolder_mouseOverHanlder(event:MouseEvent) : void {
-				btnActualSize.alpha = 100;
+				btnActualSize.alpha = 1;
 			}
 			
 			private function videoHolder_mouseOutHanlder(event:MouseEvent) : void {
@@ -256,8 +258,8 @@
 				videoHolder.width = video.width;
 				videoHolder.height = video.height;
 
-				this.verticalScrollPolicy = videoCanvas.verticalScrollPolicy = ScrollPolicy.OFF;
-				this.horizontalScrollPolicy = videoCanvas.horizontalScrollPolicy = ScrollPolicy.OFF;
+				videoCanvas.verticalScrollPolicy = ScrollPolicy.OFF;
+				videoCanvas.horizontalScrollPolicy = ScrollPolicy.OFF;
 			}
 
 			private function fitWindowToVideo():void {
@@ -284,8 +286,8 @@
 					videoHolder.height = videoHeight;
 				}
 				
-				this.verticalScrollPolicy = videoCanvas.verticalScrollPolicy = ScrollPolicy.AUTO;
-				this.horizontalScrollPolicy = videoCanvas.horizontalScrollPolicy = ScrollPolicy.AUTO;
+				videoCanvas.verticalScrollPolicy = ScrollPolicy.AUTO;
+				videoCanvas.horizontalScrollPolicy = ScrollPolicy.AUTO;
 			}
 
 			private function determineHowToDisplayVideo():void {
@@ -346,7 +348,7 @@
 			   top="{VIDEO_HEIGHT_PADDING}"
                click="determineHowToDisplayVideo()"
                selected="false"
-			   mouseOver="btnActualSize.alpha = 100"
+			   mouseOver="btnActualSize.alpha = 1"
                label="{    btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize')    }"
                toolTip="{    btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize')    }"/>
 
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml
index f5ca881c0e039e0eaab078fa62a504cfda2d36a6..1acde77ab79ba9d80b7f57e0d36e11f3a2b3ef0b 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml
@@ -30,6 +30,7 @@
 		<mate:Listener type="{UsersRollEvent.USER_ROLL_OVER}" method="onRollOver" />
 		<mate:Listener type="{UsersRollEvent.USER_ROLL_OUT}" method="onRollOut" />
 		<mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="onChangeMyRole"/>
+		<mate:Listener type="{BBBEvent.CHANGE_WEBCAMS_ONLY_FOR_MODERATOR}" method="onChangeWebcamsOnlyForModerator"/>
 	</fx:Declarations>
 	
 	<fx:Script>
@@ -47,7 +48,9 @@
 			import org.bigbluebutton.core.UsersUtil;
 			import org.bigbluebutton.core.events.LockControlEvent;
 			import org.bigbluebutton.core.events.VoiceConfEvent;
+			import org.bigbluebutton.core.model.LiveMeeting;
 			import org.bigbluebutton.core.vo.LockSettingsVO;
+			import org.bigbluebutton.main.events.BBBEvent;
 			import org.bigbluebutton.main.model.users.events.ChangeMyRole;
 			import org.bigbluebutton.main.model.users.events.ChangeRoleEvent;
 			import org.bigbluebutton.main.model.users.events.KickUserEvent;
@@ -67,7 +70,7 @@
 			private var lockRolled:Boolean = false;
 			
 			private var options:UsersOptions;
-			private var myMenu:Menu = null;
+			private var actionsMenu:Menu = null;
 
 			private function onCreationComplete():void {
 				options = Options.getOptions(UsersOptions) as UsersOptions;
@@ -109,10 +112,16 @@
 				rolledOver = false;
 				updateButtons();
 				// close the menu if it was opened
-				if (myMenu) myMenu.hide();
+				closeActionsMenu();
 
 				refreshRole(e.role == Role.MODERATOR);
 			}
+			
+			private function onChangeWebcamsOnlyForModerator(e:BBBEvent):void {
+				if (data != null) {
+					updateButtons();
+				}
+			}
 
 			private function refreshRole(amIModerator:Boolean):void {
 				lockBtn.enabled = settingsBtn.enabled = moderator = amIModerator;
@@ -177,6 +186,8 @@
 				muteImg.filters = null;
 
 				var ls:LockSettingsVO = UsersUtil.getLockSettings();
+				
+				var webcamsOnlyForModerator:Boolean = LiveMeeting.inst().meeting.webcamsOnlyForModerator;
 
 				if (data != null) {
 					settingsBtn.visible = rolledOver && !data.me && !UsersUtil.isBreakout();
@@ -231,7 +242,7 @@
 						lockBtn.includeInLayout = false;
 					}
 
-					if (data.hasStream) {
+					if (data.hasStream && !webcamsOnlyForModerator) {
 						// if it's myself or if I'm watching all the streams from the given user, then don't activate the button
 						if (data.me || data.isViewingAllStreams()) {
 							webcamImg.visible = true;
@@ -297,17 +308,17 @@
 
 			private function openSettings():void {
 				if (data != null) {
-					var myMenuData:Array = [];
+					var actionsMenuData:Array = [];
 
 					if (data.role != Role.VOICE_ONLY) {
 						if (data.role == Role.MODERATOR) {
-							myMenuData.push({
+							actionsMenuData.push({
 								label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.demoteUser',[data.name]),
 								icon: getStyle('iconDemote'),
 								callback: demoteUser
 							});
 						} else {
-							myMenuData.push({
+							actionsMenuData.push({
 								label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.promoteUser',[data.name]),
 								icon: getStyle('iconPromote'),
 								callback: promoteUser
@@ -316,7 +327,7 @@
 					}
 
 					if (options.allowKickUser) {
-						myMenuData.push({
+						actionsMenuData.push({
 							label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.kickUser',[data.name]),
 							icon: getStyle('iconEject'),
 							callback: kickUser
@@ -326,23 +337,27 @@
 					// make sure the previous menu is closed before opening a new one
 					// This could be improved to include a flag that tells if the menu is open,
 					// but it would require an extra listener for the MenuCloseEvent.
-					if (myMenu) myMenu.hide();
+					closeActionsMenu();
 
-					myMenu = Menu.createMenu(null, myMenuData, true);
-					myMenu.variableRowHeight = true;
+					actionsMenu = Menu.createMenu(null, actionsMenuData, true);
+					actionsMenu.variableRowHeight = true;
 
 					var settingsBtnPos:Point = settingsBtn.localToGlobal(new Point(0,0));
 
-					var myMenuPos:Point = new Point();
-					myMenuPos.x = settingsBtnPos.x + settingsBtn.width;
-					myMenuPos.y = settingsBtnPos.y;
+					var actionsMenuPos:Point = new Point();
+					actionsMenuPos.x = settingsBtnPos.x + settingsBtn.width;
+					actionsMenuPos.y = settingsBtnPos.y;
 
-					myMenu.addEventListener(MenuEvent.ITEM_CLICK, menuClickHandler);
-					myMenu.show(myMenuPos.x, myMenuPos.y);
-					myMenu.setFocus();
+					actionsMenu.addEventListener(MenuEvent.ITEM_CLICK, menuClickHandler);
+					actionsMenu.show(actionsMenuPos.x, actionsMenuPos.y);
+					actionsMenu.setFocus();
 				}
 			}
 
+			private function closeActionsMenu():void{
+				if (actionsMenu) actionsMenu.hide();
+			}
+			
 			private function menuClickHandler(e:MenuEvent):void {
 				e.item.callback();
 			}
diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf
index 4fe785d2855e700c9437f6c796345d8ea5c41f60..386a241d530f93c9b04d605e60c6cb24789c1376 100755
--- a/bigbluebutton-config/bin/bbb-conf
+++ b/bigbluebutton-config/bin/bbb-conf
@@ -1833,6 +1833,16 @@ if [ -n "$HOST" ]; then
 		sed -i "s/rtmpt:\/\/\([^\"\/]*\)\//rtmpt:\/\/$HOST\//g" /var/www/bigbluebutton/check/conf/config.xml
 	fi
 
+        #
+        # Update HTML5 client
+        #
+	if [ -f /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json ]; then 
+        	WS=$(cat $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*=//;p}' | sed 's/https/wss/g' | sed s'/http/ws/g')
+        	sed -i "s|\"wsUrl.*|\"wsUrl\": \"$WS/bbb-webrtc-sfu\",|g" \
+                	/usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json
+	fi
+
+
 	echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
 	stop_bigbluebutton
 	echo
diff --git a/bigbluebutton-html5/public/js/adapter.js b/bigbluebutton-html5/client/compatibility/adapter.js
similarity index 100%
rename from bigbluebutton-html5/public/js/adapter.js
rename to bigbluebutton-html5/client/compatibility/adapter.js
diff --git a/bigbluebutton-html5/public/js/adjust-videos.js b/bigbluebutton-html5/client/compatibility/adjust-videos.js
similarity index 100%
rename from bigbluebutton-html5/public/js/adjust-videos.js
rename to bigbluebutton-html5/client/compatibility/adjust-videos.js
diff --git a/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js b/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js
new file mode 100644
index 0000000000000000000000000000000000000000..a29923966fa58015032c35d3daa8f3e8c68d5696
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js
@@ -0,0 +1,601 @@
+
+var userID, callerIdName=null, conferenceVoiceBridge, userAgent=null, userMicMedia, userWebcamMedia, currentSession=null, callTimeout, callActive, callICEConnected, iceConnectedTimeout, callFailCounter, callPurposefullyEnded, uaConnected, transferTimeout, iceGatheringTimeout;
+var inEchoTest = true;
+var html5StunTurn = null;
+
+function webRTCCallback(message) {
+	switch (message.status) {
+		case 'succeded':
+			BBB.webRTCCallSucceeded();
+			break;
+		case 'failed':
+			if (message.errorcode !== 1004) {
+				message.cause = null;
+			}
+			monitorTracksStop();
+			BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause);
+			break;
+		case 'ended':
+			monitorTracksStop();
+			BBB.webRTCCallEnded(inEchoTest);
+			break;
+		case 'started':
+			monitorTracksStart();
+			BBB.webRTCCallStarted(inEchoTest);
+			break;
+		case 'connecting':
+			BBB.webRTCCallConnecting(inEchoTest);
+			break;
+		case 'waitingforice':
+			BBB.webRTCCallWaitingForICE(inEchoTest);
+			break;
+		case 'transferring':
+			BBB.webRTCCallTransferring(inEchoTest);
+			break;
+		case 'mediarequest':
+			BBB.webRTCMediaRequest();
+			break;
+		case 'mediasuccess':
+			BBB.webRTCMediaSuccess();
+			break;
+		case 'mediafail':
+			BBB.webRTCMediaFail();
+			break;
+	}
+}
+
+function callIntoConference(voiceBridge, callback, isListenOnly, stunTurn = null) {
+	// root of the call initiation process from the html5 client
+	// Flash will not pass in the listen only field. For html5 it is optional. Assume NOT listen only if no state passed
+	if (isListenOnly == null) {
+		isListenOnly = false;
+	}
+
+	// if additional stun configuration is passed, store the information
+	if (stunTurn != null) {
+		html5StunTurn = {
+			stunServers: stunTurn.stun,
+			turnServers: stunTurn.turn,
+		};
+	}
+
+	// reset callerIdName
+	callerIdName = null;
+	if (!callerIdName) {
+		BBB.getMyUserInfo(function(userInfo) {
+			console.log("User info callback [myUserID=" + userInfo.myUserID
+				+ ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL
+				+ ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter
+				+ ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge
+				+ ",isListenOnly=" + isListenOnly + "].");
+			userID = userInfo.myUserID;
+			callerIdName = userInfo.myUserID + "-bbbID-" + userInfo.myUsername;
+			if (isListenOnly) {
+				//prepend the callerIdName so it is recognized as a global audio user
+				callerIdName = "GLOBAL_AUDIO_" + callerIdName;
+			}
+			conferenceVoiceBridge = userInfo.voiceBridge
+			if (voiceBridge === "9196") {
+				voiceBridge = voiceBridge + conferenceVoiceBridge;
+			} else {
+				voiceBridge = conferenceVoiceBridge;
+			}
+			console.log(callerIdName);
+			webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
+		});
+	} else {
+		if (voiceBridge === "9196") {
+			voiceBridge = voiceBridge + conferenceVoiceBridge;
+		} else {
+			voiceBridge = conferenceVoiceBridge;
+		}
+		webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
+	}
+}
+
+function joinWebRTCVoiceConference() {
+	console.log("Joining to the voice conference directly");
+	inEchoTest = false;
+	// set proper callbacks to previously created user agent
+	if(userAgent) {
+		setUserAgentListeners(webRTCCallback);
+	}
+	callIntoConference(conferenceVoiceBridge, webRTCCallback);
+}
+
+function leaveWebRTCVoiceConference() {
+	console.log("Leaving the voice conference");
+	
+	webrtc_hangup();
+}
+
+function startWebRTCAudioTest(){
+	console.log("Joining the audio test first");
+	inEchoTest = true;
+	callIntoConference("9196", webRTCCallback);
+}
+
+function stopWebRTCAudioTest(){
+	console.log("Stopping webrtc audio test");
+	
+	webrtc_hangup();
+}
+
+function stopWebRTCAudioTestJoinConference(){
+	console.log("Transferring from audio test to conference");
+	
+	webRTCCallback({'status': 'transferring'});
+	
+	transferTimeout = setTimeout( function() {
+		console.log("Call transfer failed. No response after 3 seconds");
+		webRTCCallback({'status': 'failed', 'errorcode': 1008});
+		releaseUserMedia();
+		currentSession = null;
+		if (userAgent != null) {
+			var userAgentTemp = userAgent;
+			userAgent = null;
+			userAgentTemp.stop();
+		}
+	}, 5000);
+	
+	BBB.listen("UserJoinedVoiceEvent", userJoinedVoiceHandler);
+	
+	currentSession.dtmf(1);
+	inEchoTest = false;
+}
+
+function userJoinedVoiceHandler(event) {
+	console.log("UserJoinedVoiceHandler - " + event);
+	if (inEchoTest === false && userID === event.userID) {
+		BBB.unlisten("UserJoinedVoiceEvent", userJoinedVoiceHandler);
+		clearTimeout(transferTimeout);
+		webRTCCallback({'status': 'started'});
+	}
+}
+
+function createUA(username, server, callback, makeCallFunc) {
+	if (userAgent) {
+		console.log("User agent already created");
+		return;
+	}
+
+	console.log("Fetching STUN/TURN server info for user agent");
+
+	console.log(html5StunTurn);
+	if (html5StunTurn != null) {
+		createUAWithStuns(username, server, callback, html5StunTurn, makeCallFunc);
+		return;
+	}
+
+  BBB.getSessionToken(function(sessionToken) {
+  	$.ajax({
+  		dataType: 'json',
+  		url: '/bigbluebutton/api/stuns',
+  		data: {sessionToken:sessionToken}
+  	}).done(function(data) {
+  		var stunsConfig = {};
+  		stunsConfig['stunServers'] = ( data['stunServers'] ? data['stunServers'].map(function(data) {
+  			return data['url'];
+  		}) : [] );
+  		stunsConfig['turnServers'] = ( data['turnServers'] ? data['turnServers'].map(function(data) {
+  			return {
+  				'urls': data['url'],
+  				'username': data['username'],
+  				'password': data['password']
+  			};
+  		}) : [] );
+		//stunsConfig['remoteIceCandidates'] = ( data['remoteIceCandidates'] ? data['remoteIceCandidates'].map(function(data) {
+		//	return data['ip'];
+		//}) : [] );
+  		createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc);
+  	}).fail(function(data, textStatus, errorThrown) {
+  		BBBLog.error("Could not fetch stun/turn servers", {error: textStatus, user: callerIdName, voiceBridge: conferenceVoiceBridge});
+  		callback({'status':'failed', 'errorcode': 1009});
+  	});
+  });
+}
+
+function createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc) {
+	console.log("Creating new user agent");
+
+	/* VERY IMPORTANT 
+	 *	- You must escape the username because spaces will cause the connection to fail
+	 *	- We are connecting to the websocket through an nginx redirect instead of directly to 5066
+	 */
+	var configuration = {
+		uri: 'sip:' + encodeURIComponent(username) + '@' + server,
+		wsServers: ('https:' == document.location.protocol ? 'wss://' : 'ws://')  + server + '/ws',
+		displayName: username,
+		register: false,
+		traceSip: true,
+		autostart: false,
+		userAgentString: "BigBlueButton",
+		stunServers: stunsConfig['stunServers'],
+		turnServers: stunsConfig['turnServers'],
+		//artificialRemoteIceCandidates: stunsConfig['remoteIceCandidates']
+	};
+	
+	uaConnected = false;
+	
+	userAgent = new SIP.UA(configuration);
+	setUserAgentListeners(callback, makeCallFunc);
+	userAgent.start();
+};
+
+function setUserAgentListeners(callback, makeCallFunc) {
+	console.log("resetting UA callbacks");
+	userAgent.removeAllListeners('connected');
+	userAgent.on('connected', function() {
+		uaConnected = true;
+		callback({'status':'succeded'});
+		makeCallFunc();
+	});
+	userAgent.removeAllListeners('disconnected');
+	userAgent.on('disconnected', function() {
+		if (userAgent) {
+			if (userAgent != null) {
+				var userAgentTemp = userAgent;
+				userAgent = null;
+				userAgentTemp.stop();
+			}
+			
+			if (uaConnected) {
+				callback({'status':'failed', 'errorcode': 1001}); // WebSocket disconnected
+			} else {
+				callback({'status':'failed', 'errorcode': 1002}); // Could not make a WebSocket connection
+			}
+		}
+	});
+};
+
+function getUserMicMedia(getUserMicMediaSuccess, getUserMicMediaFailure) {
+	if (userMicMedia == undefined) {
+		if (SIP.WebRTC.isSupported()) {
+			SIP.WebRTC.getUserMedia({audio:true, video:false}, getUserMicMediaSuccess, getUserMicMediaFailure);
+		} else {
+			console.log("getUserMicMedia: webrtc not supported");
+			getUserMicMediaFailure("WebRTC is not supported");
+		}
+	} else {
+		console.log("getUserMicMedia: mic already set");
+		getUserMicMediaSuccess(userMicMedia);
+	}
+};
+
+function webrtc_call(username, voiceBridge, callback, isListenOnly) {
+	if (!isWebRTCAvailable()) {
+		callback({'status': 'failed', 'errorcode': 1003}); // Browser version not supported
+		return;
+	}
+	if (isListenOnly == null) { // assume NOT listen only unless otherwise stated
+		isListenOnly = false;
+	}
+
+	var server = window.document.location.hostname;
+	console.log("user " + username + " calling to " +  voiceBridge);
+
+	var makeCallFunc = function() {
+		// only make the call when both microphone and useragent have been created
+		// for listen only, stating listen only is a viable substitute for acquiring user media control
+		if ((isListenOnly||userMicMedia) && userAgent)
+			make_call(username, voiceBridge, server, callback, false, isListenOnly);
+	};
+
+	// Reset userAgent so we can successfully switch between listenOnly and listen+speak modes
+	userAgent = null;
+	if (!userAgent) {
+		createUA(username, server, callback, makeCallFunc);
+	}
+	// if the user requests to proceed as listen only (does not require media) or media is already acquired,
+	// proceed with making the call
+	if (isListenOnly || userMicMedia != null) {
+		makeCallFunc();
+	} else {
+		callback({'status':'mediarequest'});
+		getUserMicMedia(function(stream) {
+				console.log("getUserMicMedia: success");
+				userMicMedia = stream;
+				callback({'status':'mediasuccess'});
+				makeCallFunc();
+			}, function(e) {
+				console.error("getUserMicMedia: failure - " + e);
+				callback({'status':'mediafail', 'cause': e});
+			}
+		);
+	}
+}
+
+function make_call(username, voiceBridge, server, callback, recall, isListenOnly) {
+	if (isListenOnly == null) {
+		isListenOnly = false;
+	}
+
+	if (userAgent == null) {
+		console.log("userAgent is still null. Delaying call");
+		var callDelayTimeout = setTimeout( function() {
+			make_call(username, voiceBridge, server, callback, recall, isListenOnly);
+		}, 100);
+		return;
+	}
+
+	if (!userAgent.isConnected()) {
+		console.log("Trying to make call, but UserAgent hasn't connected yet. Delaying call");
+		userAgent.once('connected', function() {
+			console.log("UserAgent has now connected, retrying the call");
+			make_call(username, voiceBridge, server, callback, recall, isListenOnly);
+		});
+		return;
+	}
+
+	if (currentSession) {
+		console.log('Active call detected ignoring second make_call');
+		return;
+	}
+
+	// Make an audio/video call:
+	console.log("Setting options.. ");
+
+	var options = {};
+	if (isListenOnly) {
+		// create necessary options for a listen only stream
+		var stream = null;
+		// handle the web browser
+		// create a stream object through the browser separated from user media
+		if (typeof webkitMediaStream !== 'undefined') {
+			// Google Chrome
+			stream = new webkitMediaStream;
+		} else {
+			// Firefox
+			audioContext = new window.AudioContext;
+			stream = audioContext.createMediaStreamDestination().stream;
+		}
+
+		options = {
+			media: {
+				stream: stream, // use the stream created above
+				constraints: {
+					audio: true,
+					video: false
+				},
+				render: {
+					remote: document.getElementById('remote-media')
+				}
+			},
+			// a list of our RTC Connection constraints
+			RTCConstraints: {
+				// our constraints are mandatory. We must received audio and must not receive audio
+				mandatory: {
+					OfferToReceiveAudio: true,
+					OfferToReceiveVideo: false
+				}
+			}
+		};
+	} else {
+		options = {
+			media: {
+				stream: userMicMedia,
+				constraints: {
+					audio: true,
+					video: false
+				},
+				render: {
+					remote: document.getElementById('remote-media')
+				}
+			}
+		};
+	}
+	
+	callTimeout = setTimeout(function() {
+		console.log('Ten seconds without updates sending timeout code');
+		callback({'status':'failed', 'errorcode': 1006}); // Failure on call
+		releaseUserMedia();
+		currentSession = null;
+		if (userAgent != null) {
+			var userAgentTemp = userAgent;
+			userAgent = null;
+			userAgentTemp.stop();
+		}
+	}, 10000);
+	
+	callActive = false;
+	callICEConnected = false;
+	callPurposefullyEnded = false;
+	callFailCounter = 0;
+	console.log("Calling to " + voiceBridge + "....");
+	currentSession = userAgent.invite('sip:' + voiceBridge + '@' + server, options); 
+	
+	// Only send the callback if it's the first try
+	if (recall === false) {
+		console.log('call connecting');
+		callback({'status':'connecting'});
+	} else {
+		console.log('call connecting again');
+	}
+	
+	/*
+	iceGatheringTimeout = setTimeout(function() {
+		console.log('Thirty seconds without ICE gathering finishing');
+		callback({'status':'failed', 'errorcode': 1011}); // ICE Gathering Failed
+		releaseUserMedia();
+		currentSession = null;
+		if (userAgent != null) {
+			var userAgentTemp = userAgent;
+			userAgent = null;
+			userAgentTemp.stop();
+		}
+	}, 30000);
+	*/
+	
+	currentSession.mediaHandler.on('iceGatheringComplete', function() {
+		clearTimeout(iceGatheringTimeout);
+	});
+	
+	// The connecting event fires before the listener can be added
+	currentSession.on('connecting', function(){
+		clearTimeout(callTimeout);
+	});
+	currentSession.on('progress', function(response){
+		console.log('call progress: ' + response);
+		clearTimeout(callTimeout);
+	});
+	currentSession.on('failed', function(response, cause){
+		console.log('call failed with cause: '+ cause);
+		
+		if (currentSession) {
+			releaseUserMedia();
+			if (callActive === false) {
+				callback({'status':'failed', 'errorcode': 1004, 'cause': cause}); // Failure on call
+				currentSession = null;
+				if (userAgent != null) {
+					var userAgentTemp = userAgent;
+					userAgent = null;
+					userAgentTemp.stop();
+				}
+			} else {
+				callActive = false;
+				//currentSession.bye();
+				currentSession = null;
+				if (userAgent != null) {
+					userAgent.stop();
+				}
+			}
+		}
+		clearTimeout(callTimeout);
+	});
+	currentSession.on('bye', function(request){
+		callActive = false;
+		
+		if (currentSession) {
+			console.log('call ended ' + currentSession.endTime);
+			releaseUserMedia();
+			if (callPurposefullyEnded === true) {
+				callback({'status':'ended'});
+			} else {
+				callback({'status':'failed', 'errorcode': 1005}); // Call ended unexpectedly
+			}
+			clearTimeout(callTimeout);
+			currentSession = null;
+		} else {
+			console.log('bye event already received');
+		}
+	});
+	currentSession.on('cancel', function(request) {
+		callActive = false;
+
+		if (currentSession) {
+			console.log('call canceled');
+			releaseUserMedia();
+			clearTimeout(callTimeout);
+			currentSession = null;
+		} else {
+			console.log('cancel event already received');
+		}
+	});
+	currentSession.on('accepted', function(data){
+		callActive = true;
+		console.log('BigBlueButton call accepted');
+		
+		if (callICEConnected === true) {
+			callback({'status':'started'});
+		} else {
+			callback({'status':'waitingforice'});
+			console.log('Waiting for ICE negotiation');
+			iceConnectedTimeout = setTimeout(function() {
+				console.log('5 seconds without ICE finishing');
+				callback({'status':'failed', 'errorcode': 1010}); // ICE negotiation timeout
+				releaseUserMedia();
+				currentSession = null;
+				if (userAgent != null) {
+					var userAgentTemp = userAgent;
+					userAgent = null;
+					userAgentTemp.stop();
+				}
+			}, 5000);
+		}
+		clearTimeout(callTimeout);
+	});
+	currentSession.mediaHandler.on('iceConnectionFailed', function() {
+		console.log('received ice negotiation failed');
+		callback({'status':'failed', 'errorcode': 1007}); // Failure on call
+		releaseUserMedia();
+		currentSession = null;
+		clearTimeout(iceConnectedTimeout);
+		if (userAgent != null) {
+			var userAgentTemp = userAgent;
+			userAgent = null;
+			userAgentTemp.stop();
+		}
+		
+		clearTimeout(callTimeout);
+	});
+	
+	// Some browsers use status of 'connected', others use 'completed', and a couple use both
+	
+	currentSession.mediaHandler.on('iceConnectionConnected', function() {
+		console.log('Received ICE status changed to connected');
+		if (callICEConnected === false) {
+			callICEConnected = true;
+			clearTimeout(iceConnectedTimeout);
+			if (callActive === true) {
+				callback({'status':'started'});
+			}
+			clearTimeout(callTimeout);
+		}
+	});
+	
+	currentSession.mediaHandler.on('iceConnectionCompleted', function() {
+		console.log('Received ICE status changed to completed');
+		if (callICEConnected === false) {
+			callICEConnected = true;
+			clearTimeout(iceConnectedTimeout);
+			if (callActive === true) {
+				callback({'status':'started'});
+			}
+			clearTimeout(callTimeout);
+		}
+	});
+}
+
+function webrtc_hangup(callback) {
+	callPurposefullyEnded = true;
+
+	console.log("Hanging up current session");
+	if (callback) {
+	  currentSession.on('bye', callback);
+	}
+	try {
+		currentSession.bye();
+	} catch (err) {
+		console.log("Forcing to cancel current session");
+		currentSession.cancel();
+	}
+}
+
+function releaseUserMedia() {
+	if (!!userMicMedia) {
+		console.log("Releasing media tracks");
+	
+		userMicMedia.getAudioTracks().forEach(function(track) {
+			track.stop();
+		});
+
+		userMicMedia.getVideoTracks().forEach(function(track) {
+			track.stop();
+		});
+		
+		userMicMedia = null;
+	}
+}
+
+function isWebRTCAvailable() {
+	if (bowser.msedge) {
+		return false;
+	} else {
+		return SIP.WebRTC.isSupported();
+	}
+}
+
+function getCallStatus() {
+	return currentSession;
+}
+
diff --git a/bigbluebutton-html5/client/compatibility/bbblogger.js b/bigbluebutton-html5/client/compatibility/bbblogger.js
new file mode 100644
index 0000000000000000000000000000000000000000..d60d6c8ce6e22c42cc5512459f3b1a100fef01a6
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/bbblogger.js
@@ -0,0 +1,28 @@
+
+(function(window, undefined) {
+
+    var BBBLog = {};
+
+    BBBLog.critical = function (message, data) {
+      console.log(message, JSON.stringify(data));
+    }
+    
+    BBBLog.error = function (message, data) {
+      console.log(message, JSON.stringify(data));
+    }
+
+    BBBLog.warning = function (message, data) {
+      console.log(message, JSON.stringify(data));
+    }
+    
+    BBBLog.info = function (message, data) {
+      console.log(message, JSON.stringify(data));
+    }
+    
+    BBBLog.debug = function (message, data) {
+      console.log(message, JSON.stringify(data));
+    }
+    
+    window.BBBLog = BBBLog;
+})(this);
+
diff --git a/bigbluebutton-html5/client/compatibility/bowser.js b/bigbluebutton-html5/client/compatibility/bowser.js
new file mode 100644
index 0000000000000000000000000000000000000000..5b907f136f0eb41f850fba2d095f9fa9a5eb5d49
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/bowser.js
@@ -0,0 +1,601 @@
+/*!
+ * Bowser - a browser detector
+ * https://github.com/ded/bowser
+ * MIT License | (c) Dustin Diaz 2015
+ */
+
+!function (root, name, definition) {
+  if (typeof module != 'undefined' && module.exports) module.exports = definition()
+  else if (typeof define == 'function' && define.amd) define(name, definition)
+  else root[name] = definition()
+}(this, '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)
+      , nexusMobile = /nexus\s*[0-6]\s*/i.test(ua)
+      , nexusTablet = !nexusMobile && /nexus\s*[0-9]+/i.test(ua)
+      , chromeos = /CrOS/.test(ua)
+      , silk = /silk/i.test(ua)
+      , sailfish = /sailfish/i.test(ua)
+      , tizen = /tizen/i.test(ua)
+      , webos = /(web|hpw)os/i.test(ua)
+      , windowsphone = /windows phone/i.test(ua)
+      , samsungBrowser = /SamsungBrowser/i.test(ua)
+      , windows = !windowsphone && /windows/i.test(ua)
+      , mac = !iosdevice && !silk && /macintosh/i.test(ua)
+      , linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua)
+      , edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
+      , versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
+      , tablet = /tablet/i.test(ua) && !/tablet pc/i.test(ua)
+      , mobile = !tablet && /[^-]mobi/i.test(ua)
+      , xbox = /xbox/i.test(ua)
+      , result
+
+    if (/opera/i.test(ua)) {
+      //  an old Opera
+      result = {
+        name: 'Opera'
+      , opera: t
+      , version: versionIdentifier || getFirstMatch(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)
+      }
+    } else if (/opr\/|opios/i.test(ua)) {
+      // a new Opera
+      result = {
+        name: 'Opera'
+        , opera: t
+        , version: getFirstMatch(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i) || versionIdentifier
+      }
+    }
+    else if (/SamsungBrowser/i.test(ua)) {
+      result = {
+        name: 'Samsung Internet for Android'
+        , samsungBrowser: t
+        , version: versionIdentifier || getFirstMatch(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/coast/i.test(ua)) {
+      result = {
+        name: 'Opera Coast'
+        , coast: t
+        , version: versionIdentifier || getFirstMatch(/(?:coast)[\s\/](\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/yabrowser/i.test(ua)) {
+      result = {
+        name: 'Yandex Browser'
+      , yandexbrowser: t
+      , version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/ucbrowser/i.test(ua)) {
+      result = {
+          name: 'UC Browser'
+        , ucbrowser: t
+        , version: getFirstMatch(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)
+      }
+    }
+    else if (/mxios/i.test(ua)) {
+      result = {
+        name: 'Maxthon'
+        , maxthon: t
+        , version: getFirstMatch(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)
+      }
+    }
+    else if (/epiphany/i.test(ua)) {
+      result = {
+        name: 'Epiphany'
+        , epiphany: t
+        , version: getFirstMatch(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)
+      }
+    }
+    else if (/puffin/i.test(ua)) {
+      result = {
+        name: 'Puffin'
+        , puffin: t
+        , version: getFirstMatch(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)
+      }
+    }
+    else if (/sleipnir/i.test(ua)) {
+      result = {
+        name: 'Sleipnir'
+        , sleipnir: t
+        , version: getFirstMatch(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)
+      }
+    }
+    else if (/k-meleon/i.test(ua)) {
+      result = {
+        name: 'K-Meleon'
+        , kMeleon: t
+        , version: getFirstMatch(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)
+      }
+    }
+    else if (windowsphone) {
+      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 (chromeos) {
+      result = {
+        name: 'Chrome'
+      , chromeos: t
+      , chromeBook: t
+      , chrome: t
+      , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
+      }
+    } else if (/chrome.+? edge/i.test(ua)) {
+      result = {
+        name: 'Microsoft Edge'
+      , msedge: t
+      , version: edgeVersion
+      }
+    }
+    else if (/vivaldi/i.test(ua)) {
+      result = {
+        name: 'Vivaldi'
+        , vivaldi: t
+        , version: getFirstMatch(/vivaldi\/(\d+(\.\d+)?)/i) || versionIdentifier
+      }
+    }
+    else if (sailfish) {
+      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|fxios/i.test(ua)) {
+      result = {
+        name: 'Firefox'
+      , firefox: t
+      , version: getFirstMatch(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)
+      }
+      if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
+        result.firefoxos = t
+      }
+    }
+    else if (silk) {
+      result =  {
+        name: 'Amazon Silk'
+      , silk: t
+      , version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/phantom/i.test(ua)) {
+      result = {
+        name: 'PhantomJS'
+      , phantom: t
+      , version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/slimerjs/i.test(ua)) {
+      result = {
+        name: 'SlimerJS'
+        , slimer: t
+        , version: getFirstMatch(/slimerjs\/(\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 (webos) {
+      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) {
+      result = {
+        name: 'Tizen'
+      , tizen: t
+      , version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
+      };
+    }
+    else if (/qupzilla/i.test(ua)) {
+      result = {
+        name: 'QupZilla'
+        , qupzilla: t
+        , version: getFirstMatch(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i) || versionIdentifier
+      }
+    }
+    else if (/chromium/i.test(ua)) {
+      result = {
+        name: 'Chromium'
+        , chromium: t
+        , version: getFirstMatch(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i) || versionIdentifier
+      }
+    }
+    else if (/chrome|crios|crmo/i.test(ua)) {
+      result = {
+        name: 'Chrome'
+        , chrome: t
+        , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (android) {
+      result = {
+        name: 'Android'
+        , version: versionIdentifier
+      }
+    }
+    else if (/safari|applewebkit/i.test(ua)) {
+      result = {
+        name: 'Safari'
+      , safari: t
+      }
+      if (versionIdentifier) {
+        result.version = versionIdentifier
+      }
+    }
+    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(/googlebot/i.test(ua)) {
+      result = {
+        name: 'Googlebot'
+      , googlebot: t
+      , version: getFirstMatch(/googlebot\/(\d+(\.\d+))/i) || 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)) {
+      if (/(apple)?webkit\/537\.36/i.test(ua)) {
+        result.name = result.name || "Blink"
+        result.blink = t
+      } else {
+        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.windowsphone && !result.msedge && (android || result.silk)) {
+      result.android = t
+    } else if (!result.windowsphone && !result.msedge && iosdevice) {
+      result[iosdevice] = t
+      result.ios = t
+    } else if (mac) {
+      result.mac = t
+    } else if (xbox) {
+      result.xbox = t
+    } else if (windows) {
+      result.windows = t
+    } else if (linux) {
+      result.linux = t
+    }
+
+    function getWindowsVersion (s) {
+      switch (s) {
+        case 'NT': return 'NT'
+        case 'XP': return 'XP'
+        case 'NT 5.0': return '2000'
+        case 'NT 5.1': return 'XP'
+        case 'NT 5.2': return '2003'
+        case 'NT 6.0': return 'Vista'
+        case 'NT 6.1': return '7'
+        case 'NT 6.2': return '8'
+        case 'NT 6.3': return '8.1'
+        case 'NT 10.0': return '10'
+        default: return undefined
+      }
+    }
+
+    // OS version extraction
+    var osVersion = '';
+    if (result.windows) {
+      osVersion = getWindowsVersion(getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i))
+    } else if (result.windowsphone) {
+      osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
+    } else if (result.mac) {
+      osVersion = getFirstMatch(/Mac OS X (\d+([_\.\s]\d+)*)/i);
+      osVersion = osVersion.replace(/[_\s]/g, '.');
+    } 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 = !result.windows && osVersion.split('.')[0];
+    if (
+         tablet
+      || nexusTablet
+      || iosdevice == 'ipad'
+      || (android && (osMajorVersion == 3 || (osMajorVersion >= 4 && !mobile)))
+      || result.silk
+    ) {
+      result.tablet = t
+    } else if (
+         mobile
+      || iosdevice == 'iphone'
+      || iosdevice == 'ipod'
+      || android
+      || nexusMobile
+      || 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.yandexbrowser && result.version >= 15) ||
+		    (result.vivaldi && result.version >= 1.0) ||
+        (result.chrome && result.version >= 20) ||
+        (result.samsungBrowser && result.version >= 4) ||
+        (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.chromium && result.version >= 20)
+        ) {
+      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.chromium && result.version < 20)
+        ) {
+      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;
+  }
+
+  /**
+   * Get version precisions count
+   *
+   * @example
+   *   getVersionPrecision("1.10.3") // 3
+   *
+   * @param  {string} version
+   * @return {number}
+   */
+  function getVersionPrecision(version) {
+    return version.split(".").length;
+  }
+
+  /**
+   * Array::map polyfill
+   *
+   * @param  {Array} arr
+   * @param  {Function} iterator
+   * @return {Array}
+   */
+  function map(arr, iterator) {
+    var result = [], i;
+    if (Array.prototype.map) {
+      return Array.prototype.map.call(arr, iterator);
+    }
+    for (i = 0; i < arr.length; i++) {
+      result.push(iterator(arr[i]));
+    }
+    return result;
+  }
+
+  /**
+   * Calculate browser version weight
+   *
+   * @example
+   *   compareVersions(['1.10.2.1',  '1.8.2.1.90'])    // 1
+   *   compareVersions(['1.010.2.1', '1.09.2.1.90']);  // 1
+   *   compareVersions(['1.10.2.1',  '1.10.2.1']);     // 0
+   *   compareVersions(['1.10.2.1',  '1.0800.2']);     // -1
+   *
+   * @param  {Array<String>} versions versions to compare
+   * @return {Number} comparison result
+   */
+  function compareVersions(versions) {
+    // 1) get common precision for both versions, for example for "10.0" and "9" it should be 2
+    var precision = Math.max(getVersionPrecision(versions[0]), getVersionPrecision(versions[1]));
+    var chunks = map(versions, function (version) {
+      var delta = precision - getVersionPrecision(version);
+
+      // 2) "9" -> "9.0" (for precision = 2)
+      version = version + new Array(delta + 1).join(".0");
+
+      // 3) "9.0" -> ["000000000"", "000000009"]
+      return map(version.split("."), function (chunk) {
+        return new Array(20 - chunk.length).join("0") + chunk;
+      }).reverse();
+    });
+
+    // iterate in reverse order by reversed chunks array
+    while (--precision >= 0) {
+      // 4) compare: "000000009" > "000000010" = false (but "9" > "10" = true)
+      if (chunks[0][precision] > chunks[1][precision]) {
+        return 1;
+      }
+      else if (chunks[0][precision] === chunks[1][precision]) {
+        if (precision === 0) {
+          // all version chunks are same
+          return 0;
+        }
+      }
+      else {
+        return -1;
+      }
+    }
+  }
+
+  /**
+   * Check if browser is unsupported
+   *
+   * @example
+   *   bowser.isUnsupportedBrowser({
+   *     msie: "10",
+   *     firefox: "23",
+   *     chrome: "29",
+   *     safari: "5.1",
+   *     opera: "16",
+   *     phantom: "534"
+   *   });
+   *
+   * @param  {Object}  minVersions map of minimal version to browser
+   * @param  {Boolean} [strictMode = false] flag to return false if browser wasn't found in map
+   * @param  {String}  [ua] user agent string
+   * @return {Boolean}
+   */
+  function isUnsupportedBrowser(minVersions, strictMode, ua) {
+    var _bowser = bowser;
+
+    // make strictMode param optional with ua param usage
+    if (typeof strictMode === 'string') {
+      ua = strictMode;
+      strictMode = void(0);
+    }
+
+    if (strictMode === void(0)) {
+      strictMode = false;
+    }
+    if (ua) {
+      _bowser = detect(ua);
+    }
+
+    var version = "" + _bowser.version;
+    for (var browser in minVersions) {
+      if (minVersions.hasOwnProperty(browser)) {
+        if (_bowser[browser]) {
+          if (typeof minVersions[browser] !== 'string') {
+            throw new Error('Browser version in the minVersion map should be a string: ' + browser + ': ' + String(minVersions));
+          }
+
+          // browser version and min supported version.
+          return compareVersions([version, minVersions[browser]]) < 0;
+        }
+      }
+    }
+
+    return strictMode; // not found
+  }
+
+  /**
+   * Check if browser is supported
+   *
+   * @param  {Object} minVersions map of minimal version to browser
+   * @param  {Boolean} [strictMode = false] flag to return false if browser wasn't found in map
+   * @param  {String}  [ua] user agent string
+   * @return {Boolean}
+   */
+  function check(minVersions, strictMode, ua) {
+    return !isUnsupportedBrowser(minVersions, strictMode, ua);
+  }
+
+  bowser.isUnsupportedBrowser = isUnsupportedBrowser;
+  bowser.compareVersions = compareVersions;
+  bowser.check = check;
+
+  /*
+   * 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
+});
\ No newline at end of file
diff --git a/bigbluebutton-html5/client/compatibility/jquery.json-2.4.min.js b/bigbluebutton-html5/client/compatibility/jquery.json-2.4.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..98b94018f9b9166a79b5559e737277f778c6a443
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/jquery.json-2.4.min.js
@@ -0,0 +1,23 @@
+/*! jQuery JSON plugin 2.4.0 | code.google.com/p/jquery-json */
+(function($){'use strict';var escape=/["\\\x00-\x1f\x7f-\x9f]/g,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},hasOwn=Object.prototype.hasOwnProperty;$.toJSON=typeof JSON==='object'&&JSON.stringify?JSON.stringify:function(o){if(o===null){return'null';}
+    var pairs,k,name,val,type=$.type(o);if(type==='undefined'){return undefined;}
+    if(type==='number'||type==='boolean'){return String(o);}
+    if(type==='string'){return $.quoteString(o);}
+    if(typeof o.toJSON==='function'){return $.toJSON(o.toJSON());}
+    if(type==='date'){var month=o.getUTCMonth()+1,day=o.getUTCDate(),year=o.getUTCFullYear(),hours=o.getUTCHours(),minutes=o.getUTCMinutes(),seconds=o.getUTCSeconds(),milli=o.getUTCMilliseconds();if(month<10){month='0'+month;}
+        if(day<10){day='0'+day;}
+        if(hours<10){hours='0'+hours;}
+        if(minutes<10){minutes='0'+minutes;}
+        if(seconds<10){seconds='0'+seconds;}
+        if(milli<100){milli='0'+milli;}
+        if(milli<10){milli='0'+milli;}
+        return'"'+year+'-'+month+'-'+day+'T'+
+            hours+':'+minutes+':'+seconds+'.'+milli+'Z"';}
+    pairs=[];if($.isArray(o)){for(k=0;k<o.length;k++){pairs.push($.toJSON(o[k])||'null');}
+        return'['+pairs.join(',')+']';}
+    if(typeof o==='object'){for(k in o){if(hasOwn.call(o,k)){type=typeof k;if(type==='number'){name='"'+k+'"';}else if(type==='string'){name=$.quoteString(k);}else{continue;}
+        type=typeof o[k];if(type!=='function'&&type!=='undefined'){val=$.toJSON(o[k]);pairs.push(name+':'+val);}}}
+        return'{'+pairs.join(',')+'}';}};$.evalJSON=typeof JSON==='object'&&JSON.parse?JSON.parse:function(str){return eval('('+str+')');};$.secureEvalJSON=typeof JSON==='object'&&JSON.parse?JSON.parse:function(str){var filtered=str.replace(/\\["\\\/bfnrtu]/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered)){return eval('('+str+')');}
+    throw new SyntaxError('Error parsing JSON, source is not valid.');};$.quoteString=function(str){if(str.match(escape)){return'"'+str.replace(escape,function(a){var c=meta[a];if(typeof c==='string'){return c;}
+        c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
+    return'"'+str+'"';};}(jQuery));
\ No newline at end of file
diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js
new file mode 100644
index 0000000000000000000000000000000000000000..3aa4e9243994311f35964cb79379d1a53cc74f74
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js
@@ -0,0 +1,496 @@
+var isFirefox = typeof window.InstallTrigger !== 'undefined';
+var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
+var isChrome = !!window.chrome && !isOpera;
+var isSafari = navigator.userAgent.indexOf("Safari") >= 0 && !isChrome;
+var kurentoHandler = null;
+
+Kurento = function (
+    tag,
+    voiceBridge,
+    conferenceUsername,
+    internalMeetingId,
+    onFail = null,
+    chromeExtension = null
+    ) {
+
+  this.ws = null;
+  this.video;
+  this.screen;
+  this.webRtcPeer;
+  this.extensionInstalled = false;
+  this.screenConstraints = {};
+  this.mediaCallback = null;
+
+  this.voiceBridge = voiceBridge + '-SCREENSHARE';
+  this.internalMeetingId = internalMeetingId;
+
+  this.vid_width = window.screen.width;
+  this.vid_height = window.screen.height;
+
+  // TODO properly generate a uuid
+  this.sessid = Math.random().toString();
+
+  this.renderTag = 'remote-media';
+
+  this.caller_id_name = conferenceUsername;
+  this.caller_id_number = conferenceUsername;
+
+  this.kurentoPort = "bbb-webrtc-sfu";
+  this.hostName = window.location.hostname;
+  this.socketUrl = 'wss://' + this.hostName + '/' + this.kurentoPort;
+
+  this.iceServers = null;
+
+  if (chromeExtension != null) {
+    this.chromeExtension = chromeExtension;
+    window.chromeExtension = chromeExtension;
+  }
+
+  if (onFail != null) {
+    this.onFail = Kurento.normalizeCallback(onFail);
+  } else {
+    var _this = this;
+    this.onFail = function () {
+      _this.logError('Default error handler');
+    };
+  }
+};
+
+this.KurentoManager= function () {
+  this.kurentoVideo = null;
+  this.kurentoScreenshare = null;
+};
+
+KurentoManager.prototype.exitScreenShare = function () {
+  console.log("  [exitScreenShare] Exiting screensharing");
+  if(typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) {
+    if(this.kurentoScreenshare.ws !== null) {
+      this.kurentoScreenshare.ws.onclose = function(){};
+      this.kurentoScreenshare.ws.close();
+    }
+
+    this.kurentoScreenshare.disposeScreenShare();
+    this.kurentoScreenshare = null;
+  }
+
+  if (this.kurentoScreenshare) {
+    this.kurentoScreenshare = null;
+  }
+
+  if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
+    this.exitVideo();
+  }
+};
+
+KurentoManager.prototype.exitVideo = function () {
+  console.log("  [exitScreenShare] Exiting screensharing viewing");
+  if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
+    if(this.kurentoVideo.ws !== null) {
+      this.kurentoVideo.ws.onclose = function(){};
+      this.kurentoVideo.ws.close();
+    }
+
+    this.kurentoVideo.disposeScreenShare();
+    this.kurentoVideo = null;
+  }
+
+  if (this.kurentoVideo) {
+    this.kurentoVideo = null;
+  }
+};
+
+KurentoManager.prototype.shareScreen = function (tag) {
+  this.exitScreenShare();
+  var obj = Object.create(Kurento.prototype);
+  Kurento.apply(obj, arguments);
+  this.kurentoScreenshare = obj;
+  this.kurentoScreenshare.setScreenShare(tag);
+};
+
+KurentoManager.prototype.joinWatchVideo = function (tag) {
+  this.exitVideo();
+  var obj = Object.create(Kurento.prototype);
+  Kurento.apply(obj, arguments);
+  this.kurentoVideo = obj;
+  this.kurentoVideo.setWatchVideo(tag);
+};
+
+
+Kurento.prototype.setScreenShare = function (tag) {
+  this.mediaCallback = this.makeShare.bind(this);
+  this.create(tag);
+};
+
+Kurento.prototype.create = function (tag) {
+  this.setRenderTag(tag);
+  this.iceServers = true;
+  this.init();
+};
+
+Kurento.prototype.init = function () {
+  var self = this;
+  if("WebSocket" in window) {
+    console.log("this browser supports websockets");
+    this.ws = new WebSocket(this.socketUrl);
+
+    this.ws.onmessage = this.onWSMessage.bind(this);
+    this.ws.onclose = (close) => {
+      kurentoManager.exitScreenShare();
+      self.onFail("Websocket connection closed");
+    };
+    this.ws.onerror = (error) => {
+      kurentoManager.exitScreenShare();
+      self.onFail("Websocket connection error");
+    };
+    this.ws.onopen = function () {
+      self.mediaCallback();
+    }.bind(self);
+  }
+  else
+    console.log("this browser does not support websockets");
+};
+
+Kurento.prototype.onWSMessage = function (message) {
+  var parsedMessage = JSON.parse(message.data);
+  switch (parsedMessage.id) {
+
+    case 'presenterResponse':
+      this.presenterResponse(parsedMessage);
+      break;
+    case 'viewerResponse':
+      this.viewerResponse(parsedMessage);
+      break;
+    case 'stopSharing':
+      kurentoManager.exitScreenShare();
+      break;
+    case 'iceCandidate':
+      this.webRtcPeer.addIceCandidate(parsedMessage.candidate);
+      break;
+    default:
+      console.error('Unrecognized message', parsedMessage);
+  }
+};
+
+Kurento.prototype.setRenderTag = function (tag) {
+  this.renderTag = tag;
+};
+
+Kurento.prototype.presenterResponse = function (message) {
+  if (message.response != 'accepted') {
+    var errorMsg = message.message ? message.message : 'Unknown error';
+    console.warn('Call not accepted for the following reason: ' + JSON.stringify(errorMsg, null, 2));
+    kurentoManager.exitScreenShare();
+    this.onFail(errorMessage);
+  } else {
+    console.log("Presenter call was accepted with SDP => " + message.sdpAnswer);
+    this.webRtcPeer.processAnswer(message.sdpAnswer);
+  }
+}
+
+Kurento.prototype.viewerResponse = function (message) {
+  if (message.response != 'accepted') {
+    var errorMsg = message.message ? message.message : 'Unknown error';
+    console.warn('Call not accepted for the following reason: ' + errorMsg);
+    kurentoManager.exitScreenShare();
+    this.onFail(errorMessage);
+  } else {
+    console.log("Viewer call was accepted with SDP => " + message.sdpAnswer);
+    this.webRtcPeer.processAnswer(message.sdpAnswer);
+  }
+}
+
+Kurento.prototype.serverResponse = function (message) {
+  if (message.response != 'accepted') {
+    var errorMsg = message.message ? message.message : 'Unknow error';
+    console.warn('Call not accepted for the following reason: ' + errorMsg);
+    kurentoManager.exitScreenShare();
+  } else {
+    this.webRtcPeer.processAnswer(message.sdpAnswer);
+  }
+}
+
+Kurento.prototype.makeShare = function() {
+  var self = this;
+  if (!this.webRtcPeer) {
+    var options = {
+      onicecandidate : self.onIceCandidate.bind(self)
+    }
+
+    this.startScreenStreamFrom();
+  }
+}
+
+Kurento.prototype.onOfferPresenter = function (error, offerSdp) {
+  let self = this;
+  if(error)  {
+    console.log("Kurento.prototype.onOfferPresenter Error " + error);
+    this.onFail(error);
+    return;
+  }
+
+  var message = {
+    id : 'presenter',
+    type: 'screenshare',
+    role: 'presenter',
+    internalMeetingId: self.internalMeetingId,
+    voiceBridge: self.voiceBridge,
+    callerName : self.caller_id_name,
+    sdpOffer : offerSdp,
+    vh: self.vid_height,
+    vw: self.vid_width
+  };
+  console.log("onOfferPresenter sending to screenshare server => " + JSON.stringify(message, null, 2));
+  this.sendMessage(message);
+}
+
+Kurento.prototype.startScreenStreamFrom = function () {
+  var self = this;
+  if (!!window.chrome) {
+    if (!self.chromeExtension) {
+      self.logError({
+        status:  'failed',
+        message: 'Missing Chrome Extension key',
+      });
+      self.onFail();
+      return;
+    }
+  }
+  // TODO it would be nice to check those constraints
+  if (typeof screenConstraints !== undefined) {
+    self.screenConstraints = {};
+  }
+  self.screenConstraints.video = {};
+
+  console.log(self);
+  var options = {
+    localVideo: document.getElementById(this.renderTag),
+    onicecandidate : self.onIceCandidate.bind(self),
+    mediaConstraints : self.screenConstraints,
+    sendSource : 'desktop'
+  };
+
+  console.log(" Peer options => " + JSON.stringify(options, null, 2));
+
+  self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) {
+    if(error)  {
+      console.log("WebRtcPeerSendonly constructor error " + JSON.stringify(error, null, 2));
+      self.onFail(error);
+      return kurentoManager.exitScreenShare();
+    }
+
+    self.webRtcPeer.generateOffer(self.onOfferPresenter.bind(self));
+    console.log("Generated peer offer w/ options "  + JSON.stringify(options));
+  });
+}
+
+Kurento.prototype.onIceCandidate = function (candidate) {
+  let self = this;
+  console.log('Local candidate' + JSON.stringify(candidate));
+
+  var message = {
+    id : 'onIceCandidate',
+    role: 'presenter',
+    type: 'screenshare',
+    voiceBridge: self.voiceBridge,
+    candidate : candidate
+  }
+  this.sendMessage(message);
+}
+
+Kurento.prototype.onViewerIceCandidate = function (candidate) {
+  let self = this;
+  console.log('Viewer local candidate' + JSON.stringify(candidate));
+
+  var message = {
+    id : 'viewerIceCandidate',
+    role: 'viewer',
+    type: 'screenshare',
+    voiceBridge: self.voiceBridge,
+    candidate : candidate,
+    callerName: self.caller_id_name
+  }
+  this.sendMessage(message);
+}
+
+Kurento.prototype.setWatchVideo = function (tag) {
+  this.useVideo = true;
+  this.useCamera = 'none';
+  this.useMic = 'none';
+  this.mediaCallback = this.viewer;
+  this.create(tag);
+};
+
+Kurento.prototype.viewer = function () {
+  var self = this;
+  if (!this.webRtcPeer) {
+
+    var options = {
+      remoteVideo: document.getElementById(this.renderTag),
+      onicecandidate : this.onViewerIceCandidate.bind(this)
+    }
+
+    self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) {
+      if(error) {
+        return self.onFail(error);
+      }
+
+      this.generateOffer(self.onOfferViewer.bind(self));
+    });
+  }
+};
+
+Kurento.prototype.onOfferViewer = function (error, offerSdp) {
+  let self = this;
+  if(error)  {
+    console.log("Kurento.prototype.onOfferViewer Error " + error);
+    return this.onFail();
+  }
+  var message = {
+    id : 'viewer',
+    type: 'screenshare',
+    role: 'viewer',
+    internalMeetingId: self.internalMeetingId,
+    voiceBridge: self.voiceBridge,
+    callerName : self.caller_id_name,
+    sdpOffer : offerSdp
+  };
+
+  console.log("onOfferViewer sending to screenshare server => " + JSON.stringify(message, null, 2));
+  this.sendMessage(message);
+};
+
+Kurento.prototype.stop = function() {
+  //if (this.webRtcPeer) {
+  //  var message = {
+  //    id : 'stop',
+  //    type : 'screenshare',
+  //    voiceBridge: kurentoHandler.voiceBridge
+  //  }
+  //  kurentoHandler.sendMessage(message);
+  //  kurentoHandler.disposeScreenShare();
+  //}
+}
+
+Kurento.prototype.dispose = function() {
+  if (this.webRtcPeer) {
+    this.webRtcPeer.dispose();
+    this.webRtcPeer = null;
+  }
+}
+
+Kurento.prototype.disposeScreenShare = function() {
+  if (this.webRtcPeer) {
+    this.webRtcPeer.dispose();
+    this.webRtcPeer = null;
+  }
+}
+
+Kurento.prototype.sendMessage = function(message) {
+  var jsonMessage = JSON.stringify(message);
+  console.log('Sending message: ' + jsonMessage);
+  this.ws.send(jsonMessage);
+}
+
+Kurento.prototype.logger = function (obj) {
+  console.log(obj);
+};
+
+Kurento.prototype.logError = function (obj) {
+  console.error(obj);
+};
+
+
+Kurento.normalizeCallback = function (callback) {
+  if (typeof callback == 'function') {
+    return callback;
+  } else {
+    console.log(document.getElementById('BigBlueButton')[callback]);
+    return function (args) {
+      document.getElementById('BigBlueButton')[callback](args);
+    };
+  }
+};
+
+/* Global methods */
+
+// this function explains how to use above methods/objects
+window.getScreenConstraints = function(sendSource, callback) {
+  let chromeMediaSourceId = sendSource;
+  let screenConstraints = {video: {}};
+
+  if(isChrome) {
+    getChromeScreenConstraints ((constraints) => {
+      let sourceId = constraints.streamId;
+
+      // this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
+      screenConstraints.video.chromeMediaSource = { exact: [sendSource]};
+      screenConstraints.video.chromeMediaSourceId = sourceId;
+      console.log("getScreenConstraints for Chrome returns => ");
+      console.log(screenConstraints);
+      // now invoking native getUserMedia API
+      callback(null, screenConstraints);
+
+    }, chromeExtension);
+  }
+  else if (isFirefox) {
+    screenConstraints.video.mediaSource= "window";
+    screenConstraints.video.width= {max: "1280"};
+    screenConstraints.video.height = {max:  "720"};
+
+    console.log("getScreenConstraints for Firefox returns => ");
+    console.log(screenConstraints);
+    // now invoking native getUserMedia API
+    callback(null, screenConstraints);
+  }
+  else if(isSafari) {
+    screenConstraints.video.mediaSource= "screen";
+    screenConstraints.video.width= {max: window.screen.width};
+    screenConstraints.video.height = {max:  window.screen.vid_height};
+
+    console.log("getScreenConstraints for Safari returns => ");
+    console.log(screenConstraints);
+    // now invoking native getUserMedia API
+    callback(null, screenConstraints);
+  }
+}
+
+window.kurentoInitialize = function () {
+  if (window.kurentoManager == null || window.KurentoManager == undefined) {
+    window.kurentoManager = new KurentoManager();
+  }
+};
+
+window.kurentoShareScreen = function() {
+  window.kurentoInitialize();
+  window.kurentoManager.shareScreen.apply(window.kurentoManager, arguments);
+};
+
+
+window.kurentoExitScreenShare = function () {
+  window.kurentoInitialize();
+  window.kurentoManager.exitScreenShare();
+};
+
+window.kurentoWatchVideo = function () {
+  window.kurentoInitialize();
+  window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments);
+};
+
+window.kurentoExitVideo = function () {
+  window.kurentoInitialize();
+  window.kurentoManager.exitVideo();
+}
+
+window.getChromeScreenConstraints = function(callback, extensionId) {
+  chrome.runtime.sendMessage(extensionId, {
+    getStream: true,
+    sources: [
+      "window",
+      "screen",
+      "tab"
+    ]},
+    function(response) {
+      console.log(response);
+      callback(response);
+    });
+};;
diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..d171093e8784054a9f42264a0dccb960c5c25d10
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js
@@ -0,0 +1,4362 @@
+(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.kurentoUtils = 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){
+var freeice = require('freeice');
+var inherits = require('inherits');
+var UAParser = require('ua-parser-js');
+var uuid = require('uuid');
+var hark = require('hark');
+var EventEmitter = require('events').EventEmitter;
+var recursive = require('merge').recursive.bind(undefined, true);
+var sdpTranslator = require('sdp-translator');
+var logger = window.Logger || console;
+try {
+    require('kurento-browser-extensions');
+} catch (error) {
+    if (typeof getScreenConstraints === 'undefined') {
+        logger.warn('screen sharing is not available');
+        getScreenConstraints = function getScreenConstraints(sendSource, callback) {
+            callback(new Error('This library is not enabled for screen sharing'));
+        };
+    }
+}
+var MEDIA_CONSTRAINTS = {
+        audio: true,
+        video: {
+            width: 640,
+            framerate: 15
+        }
+    };
+var ua = window && window.navigator ? window.navigator.userAgent : '';
+var parser = new UAParser(ua);
+var browser = parser.getBrowser();
+var usePlanB = false;
+if (browser.name === 'Chrome' || browser.name === 'Chromium') {
+    logger.debug(browser.name + ': using SDP PlanB');
+    usePlanB = true;
+}
+function noop(error) {
+    if (error)
+        logger.error(error);
+}
+function trackStop(track) {
+    track.stop && track.stop();
+}
+function streamStop(stream) {
+    stream.getTracks().forEach(trackStop);
+}
+var dumpSDP = function (description) {
+    if (typeof description === 'undefined' || description === null) {
+        return '';
+    }
+    return 'type: ' + description.type + '\r\n' + description.sdp;
+};
+function bufferizeCandidates(pc, onerror) {
+    var candidatesQueue = [];
+    pc.addEventListener('signalingstatechange', function () {
+        if (this.signalingState === 'stable') {
+            while (candidatesQueue.length) {
+                var entry = candidatesQueue.shift();
+                this.addIceCandidate(entry.candidate, entry.callback, entry.callback);
+            }
+        }
+    });
+    return function (candidate, callback) {
+        callback = callback || onerror;
+        switch (pc.signalingState) {
+        case 'closed':
+            callback(new Error('PeerConnection object is closed'));
+            break;
+        case 'stable':
+            if (pc.remoteDescription) {
+                pc.addIceCandidate(candidate, callback, callback);
+                break;
+            }
+        default:
+            candidatesQueue.push({
+                candidate: candidate,
+                callback: callback
+            });
+        }
+    };
+}
+function removeFIDFromOffer(sdp) {
+    var n = sdp.indexOf('a=ssrc-group:FID');
+    if (n > 0) {
+        return sdp.slice(0, n);
+    } else {
+        return sdp;
+    }
+}
+function getSimulcastInfo(videoStream) {
+    var videoTracks = videoStream.getVideoTracks();
+    if (!videoTracks.length) {
+        logger.warn('No video tracks available in the video stream');
+        return '';
+    }
+    var lines = [
+            'a=x-google-flag:conference',
+            'a=ssrc-group:SIM 1 2 3',
+            'a=ssrc:1 cname:localVideo',
+            'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:1 mslabel:' + videoStream.id,
+            'a=ssrc:1 label:' + videoTracks[0].id,
+            'a=ssrc:2 cname:localVideo',
+            'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:2 mslabel:' + videoStream.id,
+            'a=ssrc:2 label:' + videoTracks[0].id,
+            'a=ssrc:3 cname:localVideo',
+            'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:3 mslabel:' + videoStream.id,
+            'a=ssrc:3 label:' + videoTracks[0].id
+        ];
+    lines.push('');
+    return lines.join('\n');
+}
+function WebRtcPeer(mode, options, callback) {
+    if (!(this instanceof WebRtcPeer)) {
+        return new WebRtcPeer(mode, options, callback);
+    }
+    WebRtcPeer.super_.call(this);
+    if (options instanceof Function) {
+        callback = options;
+        options = undefined;
+    }
+    options = options || {};
+    callback = (callback || noop).bind(this);
+    var self = this;
+    var localVideo = options.localVideo;
+    var remoteVideo = options.remoteVideo;
+    var videoStream = options.videoStream;
+    var audioStream = options.audioStream;
+    var mediaConstraints = options.mediaConstraints;
+    var connectionConstraints = options.connectionConstraints;
+    var pc = options.peerConnection;
+    var sendSource = options.sendSource || 'webcam';
+    var dataChannelConfig = options.dataChannelConfig;
+    var useDataChannels = options.dataChannels || false;
+    var dataChannel;
+    var guid = uuid.v4();
+    var configuration = recursive({ iceServers: freeice() }, options.configuration);
+    var onicecandidate = options.onicecandidate;
+    if (onicecandidate)
+        this.on('icecandidate', onicecandidate);
+    var oncandidategatheringdone = options.oncandidategatheringdone;
+    if (oncandidategatheringdone) {
+        this.on('candidategatheringdone', oncandidategatheringdone);
+    }
+    var simulcast = options.simulcast;
+    var multistream = options.multistream;
+    var interop = new sdpTranslator.Interop();
+    var candidatesQueueOut = [];
+    var candidategatheringdone = false;
+    Object.defineProperties(this, {
+        'peerConnection': {
+            get: function () {
+                return pc;
+            }
+        },
+        'id': {
+            value: options.id || guid,
+            writable: false
+        },
+        'remoteVideo': {
+            get: function () {
+                return remoteVideo;
+            }
+        },
+        'localVideo': {
+            get: function () {
+                return localVideo;
+            }
+        },
+        'dataChannel': {
+            get: function () {
+                return dataChannel;
+            }
+        },
+        'currentFrame': {
+            get: function () {
+                if (!remoteVideo)
+                    return;
+                if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
+                    throw new Error('No video stream data available');
+                var canvas = document.createElement('canvas');
+                canvas.width = remoteVideo.videoWidth;
+                canvas.height = remoteVideo.videoHeight;
+                canvas.getContext('2d').drawImage(remoteVideo, 0, 0);
+                return canvas;
+            }
+        }
+    });
+    if (!pc) {
+        pc = new RTCPeerConnection(configuration);
+        if (useDataChannels && !dataChannel) {
+            var dcId = 'WebRtcPeer-' + self.id;
+            var dcOptions = undefined;
+            if (dataChannelConfig) {
+                dcId = dataChannelConfig.id || dcId;
+                dcOptions = dataChannelConfig.options;
+            }
+            dataChannel = pc.createDataChannel(dcId, dcOptions);
+            if (dataChannelConfig) {
+                dataChannel.onopen = dataChannelConfig.onopen;
+                dataChannel.onclose = dataChannelConfig.onclose;
+                dataChannel.onmessage = dataChannelConfig.onmessage;
+                dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow;
+                dataChannel.onerror = dataChannelConfig.onerror || noop;
+            }
+        }
+    }
+    pc.addEventListener('icecandidate', function (event) {
+        var candidate = event.candidate;
+        if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) {
+            if (candidate) {
+                var cand;
+                if (multistream && usePlanB) {
+                    cand = interop.candidateToUnifiedPlan(candidate);
+                } else {
+                    cand = candidate;
+                }
+                self.emit('icecandidate', cand);
+                candidategatheringdone = false;
+            } else if (!candidategatheringdone) {
+                self.emit('candidategatheringdone');
+                candidategatheringdone = true;
+            }
+        } else if (!candidategatheringdone) {
+            candidatesQueueOut.push(candidate);
+            if (!candidate)
+                candidategatheringdone = true;
+        }
+    });
+    pc.ontrack = options.onaddstream;
+    pc.onnegotiationneeded = options.onnegotiationneeded;
+    this.on('newListener', function (event, listener) {
+        if (event === 'icecandidate' || event === 'candidategatheringdone') {
+            while (candidatesQueueOut.length) {
+                var candidate = candidatesQueueOut.shift();
+                if (!candidate === (event === 'candidategatheringdone')) {
+                    listener(candidate);
+                }
+            }
+        }
+    });
+    var addIceCandidate = bufferizeCandidates(pc);
+    this.addIceCandidate = function (iceCandidate, callback) {
+        var candidate;
+        if (multistream && usePlanB) {
+            candidate = interop.candidateToPlanB(iceCandidate);
+        } else {
+            candidate = new RTCIceCandidate(iceCandidate);
+        }
+        logger.debug('Remote ICE candidate received', iceCandidate);
+        callback = (callback || noop).bind(this);
+        addIceCandidate(candidate, callback);
+    };
+    this.generateOffer = function (callback) {
+        callback = callback.bind(this);
+        var offerAudio = true;
+        var offerVideo = true;
+        if (mediaConstraints) {
+            offerAudio = typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true;
+            offerVideo = typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true;
+        }
+        var browserDependantConstraints = {
+                offerToReceiveAudio: mode !== 'sendonly' && offerAudio,
+                offerToReceiveVideo: mode !== 'sendonly' && offerVideo
+            };
+        var constraints = browserDependantConstraints;
+        logger.debug('constraints: ' + JSON.stringify(constraints));
+        pc.createOffer(constraints).then(function (offer) {
+            logger.debug('Created SDP offer');
+            offer = mangleSdpToAddSimulcast(offer);
+            return pc.setLocalDescription(offer);
+        }).then(function () {
+            var localDescription = pc.localDescription;
+            logger.debug('Local description set', localDescription.sdp);
+            if (multistream && usePlanB) {
+                localDescription = interop.toUnifiedPlan(localDescription);
+                logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
+            }
+            callback(null, localDescription.sdp, self.processAnswer.bind(self));
+        }).catch(callback);
+    };
+    this.getLocalSessionDescriptor = function () {
+        return pc.localDescription;
+    };
+    this.getRemoteSessionDescriptor = function () {
+        return pc.remoteDescription;
+    };
+    function setRemoteVideo() {
+        if (remoteVideo) {
+            var stream = pc.getRemoteStreams()[0];
+            remoteVideo.pause();
+            remoteVideo.srcObject = stream;
+            remoteVideo.load();
+            logger.info('Remote URL:', remoteVideo.srcObject);
+        }
+    }
+    this.showLocalVideo = function () {
+        localVideo.srcObject = videoStream;
+        localVideo.muted = true;
+    };
+    this.send = function (data) {
+        if (dataChannel && dataChannel.readyState === 'open') {
+            dataChannel.send(data);
+        } else {
+            logger.warn('Trying to send data over a non-existing or closed data channel');
+        }
+    };
+    this.processAnswer = function (sdpAnswer, callback) {
+        callback = (callback || noop).bind(this);
+        var answer = new RTCSessionDescription({
+                type: 'answer',
+                sdp: sdpAnswer
+            });
+        if (multistream && usePlanB) {
+            var planBAnswer = interop.toPlanB(answer);
+            logger.debug('asnwer::planB', dumpSDP(planBAnswer));
+            answer = planBAnswer;
+        }
+        logger.debug('SDP answer received, setting remote description');
+        if (pc.signalingState === 'closed') {
+            return callback('PeerConnection is closed');
+        }
+        pc.setRemoteDescription(answer, function () {
+            setRemoteVideo();
+            callback();
+        }, callback);
+    };
+    this.processOffer = function (sdpOffer, callback) {
+        callback = callback.bind(this);
+        var offer = new RTCSessionDescription({
+                type: 'offer',
+                sdp: sdpOffer
+            });
+        if (multistream && usePlanB) {
+            var planBOffer = interop.toPlanB(offer);
+            logger.debug('offer::planB', dumpSDP(planBOffer));
+            offer = planBOffer;
+        }
+        logger.debug('SDP offer received, setting remote description');
+        if (pc.signalingState === 'closed') {
+            return callback('PeerConnection is closed');
+        }
+        pc.setRemoteDescription(offer).then(function () {
+            return setRemoteVideo();
+        }).then(function () {
+            return pc.createAnswer();
+        }).then(function (answer) {
+            answer = mangleSdpToAddSimulcast(answer);
+            logger.debug('Created SDP answer');
+            return pc.setLocalDescription(answer);
+        }).then(function () {
+            var localDescription = pc.localDescription;
+            if (multistream && usePlanB) {
+                localDescription = interop.toUnifiedPlan(localDescription);
+                logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
+            }
+            logger.debug('Local description set', localDescription.sdp);
+            callback(null, localDescription.sdp);
+        }).catch(callback);
+    };
+    function mangleSdpToAddSimulcast(answer) {
+        if (simulcast) {
+            if (browser.name === 'Chrome' || browser.name === 'Chromium') {
+                logger.debug('Adding multicast info');
+                answer = new RTCSessionDescription({
+                    'type': answer.type,
+                    'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream)
+                });
+            } else {
+                logger.warn('Simulcast is only available in Chrome browser.');
+            }
+        }
+        return answer;
+    }
+    function start() {
+        if (pc.signalingState === 'closed') {
+            callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
+        }
+        if (videoStream && localVideo) {
+            self.showLocalVideo();
+        }
+        if (videoStream) {
+            pc.addStream(videoStream);
+        }
+        if (audioStream) {
+            pc.addStream(audioStream);
+        }
+        var browser = parser.getBrowser();
+        if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) {
+            mode = 'sendrecv';
+        }
+        callback();
+    }
+    if (mode !== 'recvonly' && !videoStream && !audioStream) {
+        function getMedia(constraints) {
+            if (constraints === undefined) {
+                constraints = MEDIA_CONSTRAINTS;
+            }
+            navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
+                videoStream = stream;
+                start();
+            }).catch(callback);
+        }
+        if (sendSource === 'webcam') {
+            getMedia(mediaConstraints);
+        } else {
+            getScreenConstraints(sendSource, function (error, constraints_) {
+                if (error)
+                    return callback(error);
+                constraints = [mediaConstraints];
+                constraints.unshift(constraints_);
+                getMedia(recursive.apply(undefined, constraints));
+            }, guid);
+        }
+    } else {
+        setTimeout(start, 0);
+    }
+    this.on('_dispose', function () {
+        if (localVideo) {
+            localVideo.pause();
+            localVideo.src = '';
+            localVideo.load();
+            localVideo.muted = false;
+        }
+        if (remoteVideo) {
+            remoteVideo.pause();
+            remoteVideo.src = '';
+            remoteVideo.load();
+        }
+        self.removeAllListeners();
+        if (window.cancelChooseDesktopMedia !== undefined) {
+            window.cancelChooseDesktopMedia(guid);
+        }
+    });
+}
+inherits(WebRtcPeer, EventEmitter);
+function createEnableDescriptor(type) {
+    var method = 'get' + type + 'Tracks';
+    return {
+        enumerable: true,
+        get: function () {
+            if (!this.peerConnection)
+                return;
+            var streams = this.peerConnection.getLocalStreams();
+            if (!streams.length)
+                return;
+            for (var i = 0, stream; stream = streams[i]; i++) {
+                var tracks = stream[method]();
+                for (var j = 0, track; track = tracks[j]; j++)
+                    if (!track.enabled)
+                        return false;
+            }
+            return true;
+        },
+        set: function (value) {
+            function trackSetEnable(track) {
+                track.enabled = value;
+            }
+            this.peerConnection.getLocalStreams().forEach(function (stream) {
+                stream[method]().forEach(trackSetEnable);
+            });
+        }
+    };
+}
+Object.defineProperties(WebRtcPeer.prototype, {
+    'enabled': {
+        enumerable: true,
+        get: function () {
+            return this.audioEnabled && this.videoEnabled;
+        },
+        set: function (value) {
+            this.audioEnabled = this.videoEnabled = value;
+        }
+    },
+    'audioEnabled': createEnableDescriptor('Audio'),
+    'videoEnabled': createEnableDescriptor('Video')
+});
+WebRtcPeer.prototype.getLocalStream = function (index) {
+    if (this.peerConnection) {
+        return this.peerConnection.getLocalStreams()[index || 0];
+    }
+};
+WebRtcPeer.prototype.getRemoteStream = function (index) {
+    if (this.peerConnection) {
+        return this.peerConnection.getRemoteStreams()[index || 0];
+    }
+};
+WebRtcPeer.prototype.dispose = function () {
+    logger.debug('Disposing WebRtcPeer');
+    var pc = this.peerConnection;
+    var dc = this.dataChannel;
+    try {
+        if (dc) {
+            if (dc.signalingState === 'closed')
+                return;
+            dc.close();
+        }
+        if (pc) {
+            if (pc.signalingState === 'closed')
+                return;
+            pc.getLocalStreams().forEach(streamStop);
+            pc.close();
+        }
+    } catch (err) {
+        logger.warn('Exception disposing webrtc peer ' + err);
+    }
+    this.emit('_dispose');
+};
+function WebRtcPeerRecvonly(options, callback) {
+    if (!(this instanceof WebRtcPeerRecvonly)) {
+        return new WebRtcPeerRecvonly(options, callback);
+    }
+    WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback);
+}
+inherits(WebRtcPeerRecvonly, WebRtcPeer);
+function WebRtcPeerSendonly(options, callback) {
+    if (!(this instanceof WebRtcPeerSendonly)) {
+        return new WebRtcPeerSendonly(options, callback);
+    }
+    WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback);
+}
+inherits(WebRtcPeerSendonly, WebRtcPeer);
+function WebRtcPeerSendrecv(options, callback) {
+    if (!(this instanceof WebRtcPeerSendrecv)) {
+        return new WebRtcPeerSendrecv(options, callback);
+    }
+    WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback);
+}
+inherits(WebRtcPeerSendrecv, WebRtcPeer);
+function harkUtils(stream, options) {
+    return hark(stream, options);
+}
+exports.bufferizeCandidates = bufferizeCandidates;
+exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly;
+exports.WebRtcPeerSendonly = WebRtcPeerSendonly;
+exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv;
+exports.hark = harkUtils;
+},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){
+if (window.addEventListener)
+    module.exports = require('./index');
+},{"./index":3}],3:[function(require,module,exports){
+var WebRtcPeer = require('./WebRtcPeer');
+exports.WebRtcPeer = WebRtcPeer;
+},{"./WebRtcPeer":1}],4:[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
+      } else {
+        // At least give some kind of context to the user
+        var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
+        err.context = er;
+        throw err;
+      }
+    }
+  }
+
+  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:
+        args = Array.prototype.slice.call(arguments, 1);
+        handler.apply(this, args);
+    }
+  } else if (isObject(handler)) {
+    args = Array.prototype.slice.call(arguments, 1);
+    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) {
+    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 if (listeners) {
+    // 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.prototype.listenerCount = function(type) {
+  if (this._events) {
+    var evlistener = this._events[type];
+
+    if (isFunction(evlistener))
+      return 1;
+    else if (evlistener)
+      return evlistener.length;
+  }
+  return 0;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  return emitter.listenerCount(type);
+};
+
+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;
+}
+
+},{}],5:[function(require,module,exports){
+/* jshint node: true */
+'use strict';
+
+var normalice = require('normalice');
+
+/**
+  # freeice
+
+  The `freeice` module is a simple way of getting random STUN or TURN server
+  for your WebRTC application.  The list of servers (just STUN at this stage)
+  were sourced from this [gist](https://gist.github.com/zziuni/3741933).
+
+  ## Example Use
+
+  The following demonstrates how you can use `freeice` with
+  [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect):
+
+  <<< examples/quickconnect.js
+
+  As the `freeice` module generates ice servers in a list compliant with the
+  WebRTC spec you will be able to use it with raw `RTCPeerConnection`
+  constructors and other WebRTC libraries.
+
+  ## Hey, don't use my STUN/TURN server!
+
+  If for some reason your free STUN or TURN server ends up in the
+  list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or
+  [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json))
+  that is used in this module, you can feel
+  free to open an issue on this repository and those servers will be removed
+  within 24 hours (or sooner).  This is the quickest and probably the most
+  polite way to have something removed (and provides us some visibility
+  if someone opens a pull request requesting that a server is added).
+
+  ## Please add my server!
+
+  If you have a server that you wish to add to the list, that's awesome! I'm
+  sure I speak on behalf of a whole pile of WebRTC developers who say thanks.
+  To get it into the list, feel free to either open a pull request or if you
+  find that process a bit daunting then just create an issue requesting
+  the addition of the server (make sure you provide all the details, and if
+  you have a Terms of Service then including that in the PR/issue would be
+  awesome).
+
+  ## I know of a free server, can I add it?
+
+  Sure, if you do your homework and make sure it is ok to use (I'm currently
+  in the process of reviewing the terms of those STUN servers included from
+  the original list).  If it's ok to go, then please see the previous entry
+  for how to add it.
+
+  ## Current List of Servers
+
+  * current as at the time of last `README.md` file generation
+
+  ### STUN
+
+  <<< stun.json
+
+  ### TURN
+
+  <<< turn.json
+
+**/
+
+var freeice = module.exports = function(opts) {
+  // if a list of servers has been provided, then use it instead of defaults
+  var servers = {
+    stun: (opts || {}).stun || require('./stun.json'),
+    turn: (opts || {}).turn || require('./turn.json')
+  };
+
+  var stunCount = (opts || {}).stunCount || 2;
+  var turnCount = (opts || {}).turnCount || 0;
+  var selected;
+
+  function getServers(type, count) {
+    var out = [];
+    var input = [].concat(servers[type]);
+    var idx;
+
+    while (input.length && out.length < count) {
+      idx = (Math.random() * input.length) | 0;
+      out = out.concat(input.splice(idx, 1));
+    }
+
+    return out.map(function(url) {
+        //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up
+        if ((typeof url !== 'string') && (! (url instanceof String))) {
+            return url;
+        } else {
+            return normalice(type + ':' + url);
+        }
+    });
+  }
+
+  // add stun servers
+  selected = [].concat(getServers('stun', stunCount));
+
+  if (turnCount) {
+    selected = selected.concat(getServers('turn', turnCount));
+  }
+
+  return selected;
+};
+
+},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){
+module.exports=[
+  "stun.l.google.com:19302",
+  "stun1.l.google.com:19302",
+  "stun2.l.google.com:19302",
+  "stun3.l.google.com:19302",
+  "stun4.l.google.com:19302",
+  "stun.ekiga.net",
+  "stun.ideasip.com",
+  "stun.schlund.de",
+  "stun.stunprotocol.org:3478",
+  "stun.voiparound.com",
+  "stun.voipbuster.com",
+  "stun.voipstunt.com",
+  "stun.voxgratia.org",
+  "stun.services.mozilla.com"
+]
+
+},{}],7:[function(require,module,exports){
+module.exports=[]
+
+},{}],8:[function(require,module,exports){
+var WildEmitter = require('wildemitter');
+
+function getMaxVolume (analyser, fftBins) {
+  var maxVolume = -Infinity;
+  analyser.getFloatFrequencyData(fftBins);
+
+  for(var i=4, ii=fftBins.length; i < ii; i++) {
+    if (fftBins[i] > maxVolume && fftBins[i] < 0) {
+      maxVolume = fftBins[i];
+    }
+  };
+
+  return maxVolume;
+}
+
+
+var audioContextType = window.AudioContext || window.webkitAudioContext;
+// use a single audio context due to hardware limits
+var audioContext = null;
+module.exports = function(stream, options) {
+  var harker = new WildEmitter();
+
+
+  // make it not break in non-supported browsers
+  if (!audioContextType) return harker;
+
+  //Config
+  var options = options || {},
+      smoothing = (options.smoothing || 0.1),
+      interval = (options.interval || 50),
+      threshold = options.threshold,
+      play = options.play,
+      history = options.history || 10,
+      running = true;
+
+  //Setup Audio Context
+  if (!audioContext) {
+    audioContext = new audioContextType();
+  }
+  var sourceNode, fftBins, analyser;
+
+  analyser = audioContext.createAnalyser();
+  analyser.fftSize = 512;
+  analyser.smoothingTimeConstant = smoothing;
+  fftBins = new Float32Array(analyser.fftSize);
+
+  if (stream.jquery) stream = stream[0];
+  if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
+    //Audio Tag
+    sourceNode = audioContext.createMediaElementSource(stream);
+    if (typeof play === 'undefined') play = true;
+    threshold = threshold || -50;
+  } else {
+    //WebRTC Stream
+    sourceNode = audioContext.createMediaStreamSource(stream);
+    threshold = threshold || -50;
+  }
+
+  sourceNode.connect(analyser);
+  if (play) analyser.connect(audioContext.destination);
+
+  harker.speaking = false;
+
+  harker.setThreshold = function(t) {
+    threshold = t;
+  };
+
+  harker.setInterval = function(i) {
+    interval = i;
+  };
+  
+  harker.stop = function() {
+    running = false;
+    harker.emit('volume_change', -100, threshold);
+    if (harker.speaking) {
+      harker.speaking = false;
+      harker.emit('stopped_speaking');
+    }
+  };
+  harker.speakingHistory = [];
+  for (var i = 0; i < history; i++) {
+      harker.speakingHistory.push(0);
+  }
+
+  // Poll the analyser node to determine if speaking
+  // and emit events if changed
+  var looper = function() {
+    setTimeout(function() {
+    
+      //check if stop has been called
+      if(!running) {
+        return;
+      }
+      
+      var currentVolume = getMaxVolume(analyser, fftBins);
+
+      harker.emit('volume_change', currentVolume, threshold);
+
+      var history = 0;
+      if (currentVolume > threshold && !harker.speaking) {
+        // trigger quickly, short history
+        for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
+          history += harker.speakingHistory[i];
+        }
+        if (history >= 2) {
+          harker.speaking = true;
+          harker.emit('speaking');
+        }
+      } else if (currentVolume < threshold && harker.speaking) {
+        for (var i = 0; i < harker.speakingHistory.length; i++) {
+          history += harker.speakingHistory[i];
+        }
+        if (history == 0) {
+          harker.speaking = false;
+          harker.emit('stopped_speaking');
+        }
+      }
+      harker.speakingHistory.shift();
+      harker.speakingHistory.push(0 + (currentVolume > threshold));
+
+      looper();
+    }, interval);
+  };
+  looper();
+
+
+  return harker;
+}
+
+},{"wildemitter":24}],9:[function(require,module,exports){
+if (typeof Object.create === 'function') {
+  // implementation from standard node.js 'util' module
+  module.exports = function inherits(ctor, superCtor) {
+    ctor.super_ = superCtor
+    ctor.prototype = Object.create(superCtor.prototype, {
+      constructor: {
+        value: ctor,
+        enumerable: false,
+        writable: true,
+        configurable: true
+      }
+    });
+  };
+} else {
+  // old school shim for old browsers
+  module.exports = function inherits(ctor, superCtor) {
+    ctor.super_ = superCtor
+    var TempCtor = function () {}
+    TempCtor.prototype = superCtor.prototype
+    ctor.prototype = new TempCtor()
+    ctor.prototype.constructor = ctor
+  }
+}
+
+},{}],10:[function(require,module,exports){
+// Does nothing at all.
+
+},{}],11:[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);
+},{}],12:[function(require,module,exports){
+/**
+  # normalice
+
+  Normalize an ice server configuration object (or plain old string) into a format
+  that is usable in all browsers supporting WebRTC.  Primarily this module is designed
+  to help with the transition of the `url` attribute of the configuration object to
+  the `urls` attribute.
+
+  ## Example Usage
+
+  <<< examples/simple.js
+
+**/
+
+var protocols = [
+  'stun:',
+  'turn:'
+];
+
+module.exports = function(input) {
+  var url = (input || {}).url || input;
+  var protocol;
+  var parts;
+  var output = {};
+
+  // if we don't have a string url, then allow the input to passthrough
+  if (typeof url != 'string' && (! (url instanceof String))) {
+    return input;
+  }
+
+  // trim the url string, and convert to an array
+  url = url.trim();
+
+  // if the protocol is not known, then passthrough
+  protocol = protocols[protocols.indexOf(url.slice(0, 5))];
+  if (! protocol) {
+    return input;
+  }
+
+  // now let's attack the remaining url parts
+  url = url.slice(5);
+  parts = url.split('@');
+
+  output.username = input.username;
+  output.credential = input.credential;
+  // if we have an authentication part, then set the credentials
+  if (parts.length > 1) {
+    url = parts[1];
+    parts = parts[0].split(':');
+
+    // add the output credential and username
+    output.username = parts[0];
+    output.credential = (input || {}).credential || parts[1] || '';
+  }
+
+  output.url = protocol + url;
+  output.urls = [ output.url ];
+
+  return output;
+};
+
+},{}],13:[function(require,module,exports){
+var grammar = module.exports = {
+  v: [{
+      name: 'version',
+      reg: /^(\d*)$/
+  }],
+  o: [{ //o=- 20518 0 IN IP4 203.0.113.1
+    // NB: sessionId will be a String in most cases because it is huge
+    name: 'origin',
+    reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
+    names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
+    format: "%s %s %d %s IP%d %s"
+  }],
+  // default parsing of these only (though some of these feel outdated)
+  s: [{ name: 'name' }],
+  i: [{ name: 'description' }],
+  u: [{ name: 'uri' }],
+  e: [{ name: 'email' }],
+  p: [{ name: 'phone' }],
+  z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
+  r: [{ name: 'repeats' }],   // TODO: this one can also be parsed properly
+  //k: [{}], // outdated thing ignored
+  t: [{ //t=0 0
+    name: 'timing',
+    reg: /^(\d*) (\d*)/,
+    names: ['start', 'stop'],
+    format: "%d %d"
+  }],
+  c: [{ //c=IN IP4 10.47.197.26
+      name: 'connection',
+      reg: /^IN IP(\d) (\S*)/,
+      names: ['version', 'ip'],
+      format: "IN IP%d %s"
+  }],
+  b: [{ //b=AS:4000
+      push: 'bandwidth',
+      reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
+      names: ['type', 'limit'],
+      format: "%s:%s"
+  }],
+  m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
+      // NB: special - pushes to session
+      // TODO: rtp/fmtp should be filtered by the payloads found here?
+      reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
+      names: ['type', 'port', 'protocol', 'payloads'],
+      format: "%s %d %s %s"
+  }],
+  a: [
+    { //a=rtpmap:110 opus/48000/2
+      push: 'rtp',
+      reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
+      names: ['payload', 'codec', 'rate', 'encoding'],
+      format: function (o) {
+        return (o.encoding) ?
+          "rtpmap:%d %s/%s/%s":
+          o.rate ?
+          "rtpmap:%d %s/%s":
+          "rtpmap:%d %s";
+      }
+    },
+    {
+      //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
+      //a=fmtp:111 minptime=10; useinbandfec=1
+      push: 'fmtp',
+      reg: /^fmtp:(\d*) ([\S| ]*)/,
+      names: ['payload', 'config'],
+      format: "fmtp:%d %s"
+    },
+    { //a=control:streamid=0
+        name: 'control',
+        reg: /^control:(.*)/,
+        format: "control:%s"
+    },
+    { //a=rtcp:65179 IN IP4 193.84.77.194
+      name: 'rtcp',
+      reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
+      names: ['port', 'netType', 'ipVer', 'address'],
+      format: function (o) {
+        return (o.address != null) ?
+          "rtcp:%d %s IP%d %s":
+          "rtcp:%d";
+      }
+    },
+    { //a=rtcp-fb:98 trr-int 100
+      push: 'rtcpFbTrrInt',
+      reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
+      names: ['payload', 'value'],
+      format: "rtcp-fb:%d trr-int %d"
+    },
+    { //a=rtcp-fb:98 nack rpsi
+      push: 'rtcpFb',
+      reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
+      names: ['payload', 'type', 'subtype'],
+      format: function (o) {
+        return (o.subtype != null) ?
+          "rtcp-fb:%s %s %s":
+          "rtcp-fb:%s %s";
+      }
+    },
+    { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+      //a=extmap:1/recvonly URI-gps-string
+      push: 'ext',
+      reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
+      names: ['value', 'uri', 'config'], // value may include "/direction" suffix
+      format: function (o) {
+        return (o.config != null) ?
+          "extmap:%s %s %s":
+          "extmap:%s %s";
+      }
+    },
+    {
+      //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
+      push: 'crypto',
+      reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
+      names: ['id', 'suite', 'config', 'sessionConfig'],
+      format: function (o) {
+        return (o.sessionConfig != null) ?
+          "crypto:%d %s %s %s":
+          "crypto:%d %s %s";
+      }
+    },
+    { //a=setup:actpass
+      name: 'setup',
+      reg: /^setup:(\w*)/,
+      format: "setup:%s"
+    },
+    { //a=mid:1
+      name: 'mid',
+      reg: /^mid:([^\s]*)/,
+      format: "mid:%s"
+    },
+    { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
+      name: 'msid',
+      reg: /^msid:(.*)/,
+      format: "msid:%s"
+    },
+    { //a=ptime:20
+      name: 'ptime',
+      reg: /^ptime:(\d*)/,
+      format: "ptime:%d"
+    },
+    { //a=maxptime:60
+      name: 'maxptime',
+      reg: /^maxptime:(\d*)/,
+      format: "maxptime:%d"
+    },
+    { //a=sendrecv
+      name: 'direction',
+      reg: /^(sendrecv|recvonly|sendonly|inactive)/
+    },
+    { //a=ice-lite
+      name: 'icelite',
+      reg: /^(ice-lite)/
+    },
+    { //a=ice-ufrag:F7gI
+      name: 'iceUfrag',
+      reg: /^ice-ufrag:(\S*)/,
+      format: "ice-ufrag:%s"
+    },
+    { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
+      name: 'icePwd',
+      reg: /^ice-pwd:(\S*)/,
+      format: "ice-pwd:%s"
+    },
+    { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
+      name: 'fingerprint',
+      reg: /^fingerprint:(\S*) (\S*)/,
+      names: ['type', 'hash'],
+      format: "fingerprint:%s %s"
+    },
+    {
+      //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
+      //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
+      //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
+      //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
+      //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
+      push:'candidates',
+      reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
+      names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
+      format: function (o) {
+        var str = "candidate:%s %d %s %d %s %d typ %s";
+
+        str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
+
+        // NB: candidate has three optional chunks, so %void middles one if it's missing
+        str += (o.tcptype != null) ? " tcptype %s" : "%v";
+
+        if (o.generation != null) {
+          str += " generation %d";
+        }
+        return str;
+      }
+    },
+    { //a=end-of-candidates (keep after the candidates line for readability)
+      name: 'endOfCandidates',
+      reg: /^(end-of-candidates)/
+    },
+    { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
+      name: 'remoteCandidates',
+      reg: /^remote-candidates:(.*)/,
+      format: "remote-candidates:%s"
+    },
+    { //a=ice-options:google-ice
+      name: 'iceOptions',
+      reg: /^ice-options:(\S*)/,
+      format: "ice-options:%s"
+    },
+    { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
+      push: "ssrcs",
+      reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
+      names: ['id', 'attribute', 'value'],
+      format: "ssrc:%d %s:%s"
+    },
+    { //a=ssrc-group:FEC 1 2
+      push: "ssrcGroups",
+      reg: /^ssrc-group:(\w*) (.*)/,
+      names: ['semantics', 'ssrcs'],
+      format: "ssrc-group:%s %s"
+    },
+    { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
+      name: "msidSemantic",
+      reg: /^msid-semantic:\s?(\w*) (\S*)/,
+      names: ['semantic', 'token'],
+      format: "msid-semantic: %s %s" // space after ":" is not accidental
+    },
+    { //a=group:BUNDLE audio video
+      push: 'groups',
+      reg: /^group:(\w*) (.*)/,
+      names: ['type', 'mids'],
+      format: "group:%s %s"
+    },
+    { //a=rtcp-mux
+      name: 'rtcpMux',
+      reg: /^(rtcp-mux)/
+    },
+    { //a=rtcp-rsize
+      name: 'rtcpRsize',
+      reg: /^(rtcp-rsize)/
+    },
+    { // any a= that we don't understand is kepts verbatim on media.invalid
+      push: 'invalid',
+      names: ["value"]
+    }
+  ]
+};
+
+// set sensible defaults to avoid polluting the grammar with boring details
+Object.keys(grammar).forEach(function (key) {
+  var objs = grammar[key];
+  objs.forEach(function (obj) {
+    if (!obj.reg) {
+      obj.reg = /(.*)/;
+    }
+    if (!obj.format) {
+      obj.format = "%s";
+    }
+  });
+});
+
+},{}],14:[function(require,module,exports){
+var parser = require('./parser');
+var writer = require('./writer');
+
+exports.write = writer;
+exports.parse = parser.parse;
+exports.parseFmtpConfig = parser.parseFmtpConfig;
+exports.parsePayloads = parser.parsePayloads;
+exports.parseRemoteCandidates = parser.parseRemoteCandidates;
+
+},{"./parser":15,"./writer":16}],15:[function(require,module,exports){
+var toIntIfInt = function (v) {
+  return String(Number(v)) === v ? Number(v) : v;
+};
+
+var attachProperties = function (match, location, names, rawName) {
+  if (rawName && !names) {
+    location[rawName] = toIntIfInt(match[1]);
+  }
+  else {
+    for (var i = 0; i < names.length; i += 1) {
+      if (match[i+1] != null) {
+        location[names[i]] = toIntIfInt(match[i+1]);
+      }
+    }
+  }
+};
+
+var parseReg = function (obj, location, content) {
+  var needsBlank = obj.name && obj.names;
+  if (obj.push && !location[obj.push]) {
+    location[obj.push] = [];
+  }
+  else if (needsBlank && !location[obj.name]) {
+    location[obj.name] = {};
+  }
+  var keyLocation = obj.push ?
+    {} :  // blank object that will be pushed
+    needsBlank ? location[obj.name] : location; // otherwise, named location or root
+
+  attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
+
+  if (obj.push) {
+    location[obj.push].push(keyLocation);
+  }
+};
+
+var grammar = require('./grammar');
+var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
+
+exports.parse = function (sdp) {
+  var session = {}
+    , media = []
+    , location = session; // points at where properties go under (one of the above)
+
+  // parse lines we understand
+  sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
+    var type = l[0];
+    var content = l.slice(2);
+    if (type === 'm') {
+      media.push({rtp: [], fmtp: []});
+      location = media[media.length-1]; // point at latest media line
+    }
+
+    for (var j = 0; j < (grammar[type] || []).length; j += 1) {
+      var obj = grammar[type][j];
+      if (obj.reg.test(content)) {
+        return parseReg(obj, location, content);
+      }
+    }
+  });
+
+  session.media = media; // link it up
+  return session;
+};
+
+var fmtpReducer = function (acc, expr) {
+  var s = expr.split('=');
+  if (s.length === 2) {
+    acc[s[0]] = toIntIfInt(s[1]);
+  }
+  return acc;
+};
+
+exports.parseFmtpConfig = function (str) {
+  return str.split(/\;\s?/).reduce(fmtpReducer, {});
+};
+
+exports.parsePayloads = function (str) {
+  return str.split(' ').map(Number);
+};
+
+exports.parseRemoteCandidates = function (str) {
+  var candidates = [];
+  var parts = str.split(' ').map(toIntIfInt);
+  for (var i = 0; i < parts.length; i += 3) {
+    candidates.push({
+      component: parts[i],
+      ip: parts[i + 1],
+      port: parts[i + 2]
+    });
+  }
+  return candidates;
+};
+
+},{"./grammar":13}],16:[function(require,module,exports){
+var grammar = require('./grammar');
+
+// customized util.format - discards excess arguments and can void middle ones
+var formatRegExp = /%[sdv%]/g;
+var format = function (formatStr) {
+  var i = 1;
+  var args = arguments;
+  var len = args.length;
+  return formatStr.replace(formatRegExp, function (x) {
+    if (i >= len) {
+      return x; // missing argument
+    }
+    var arg = args[i];
+    i += 1;
+    switch (x) {
+      case '%%':
+        return '%';
+      case '%s':
+        return String(arg);
+      case '%d':
+        return Number(arg);
+      case '%v':
+        return '';
+    }
+  });
+  // NB: we discard excess arguments - they are typically undefined from makeLine
+};
+
+var makeLine = function (type, obj, location) {
+  var str = obj.format instanceof Function ?
+    (obj.format(obj.push ? location : location[obj.name])) :
+    obj.format;
+
+  var args = [type + '=' + str];
+  if (obj.names) {
+    for (var i = 0; i < obj.names.length; i += 1) {
+      var n = obj.names[i];
+      if (obj.name) {
+        args.push(location[obj.name][n]);
+      }
+      else { // for mLine and push attributes
+        args.push(location[obj.names[i]]);
+      }
+    }
+  }
+  else {
+    args.push(location[obj.name]);
+  }
+  return format.apply(null, args);
+};
+
+// RFC specified order
+// TODO: extend this with all the rest
+var defaultOuterOrder = [
+  'v', 'o', 's', 'i',
+  'u', 'e', 'p', 'c',
+  'b', 't', 'r', 'z', 'a'
+];
+var defaultInnerOrder = ['i', 'c', 'b', 'a'];
+
+
+module.exports = function (session, opts) {
+  opts = opts || {};
+  // ensure certain properties exist
+  if (session.version == null) {
+    session.version = 0; // "v=0" must be there (only defined version atm)
+  }
+  if (session.name == null) {
+    session.name = " "; // "s= " must be there if no meaningful name set
+  }
+  session.media.forEach(function (mLine) {
+    if (mLine.payloads == null) {
+      mLine.payloads = "";
+    }
+  });
+
+  var outerOrder = opts.outerOrder || defaultOuterOrder;
+  var innerOrder = opts.innerOrder || defaultInnerOrder;
+  var sdp = [];
+
+  // loop through outerOrder for matching properties on session
+  outerOrder.forEach(function (type) {
+    grammar[type].forEach(function (obj) {
+      if (obj.name in session && session[obj.name] != null) {
+        sdp.push(makeLine(type, obj, session));
+      }
+      else if (obj.push in session && session[obj.push] != null) {
+        session[obj.push].forEach(function (el) {
+          sdp.push(makeLine(type, obj, el));
+        });
+      }
+    });
+  });
+
+  // then for each media line, follow the innerOrder
+  session.media.forEach(function (mLine) {
+    sdp.push(makeLine('m', grammar.m[0], mLine));
+
+    innerOrder.forEach(function (type) {
+      grammar[type].forEach(function (obj) {
+        if (obj.name in mLine && mLine[obj.name] != null) {
+          sdp.push(makeLine(type, obj, mLine));
+        }
+        else if (obj.push in mLine && mLine[obj.push] != null) {
+          mLine[obj.push].forEach(function (el) {
+            sdp.push(makeLine(type, obj, el));
+          });
+        }
+      });
+    });
+  });
+
+  return sdp.join('\r\n') + '\r\n';
+};
+
+},{"./grammar":13}],17:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = function arrayEquals(array) {
+    // if the other array is a falsy value, return
+    if (!array)
+        return false;
+
+    // compare lengths - can save a lot of time
+    if (this.length != array.length)
+        return false;
+
+    for (var i = 0, l = this.length; i < l; i++) {
+        // Check if we have nested arrays
+        if (this[i] instanceof Array && array[i] instanceof Array) {
+            // recurse into the nested arrays
+            if (!arrayEquals.apply(this[i], [array[i]]))
+                return false;
+        } else if (this[i] != array[i]) {
+            // Warning - two different object instances will never be equal:
+            // {x:20} != {x:20}
+            return false;
+        }
+    }
+    return true;
+};
+
+
+},{}],18:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+exports.Interop = require('./interop');
+
+},{"./interop":19}],19:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global RTCSessionDescription */
+/* global RTCIceCandidate */
+/* jshint -W097 */
+"use strict";
+
+var transform = require('./transform');
+var arrayEquals = require('./array-equals');
+
+function Interop() {
+
+    /**
+     * This map holds the most recent Unified Plan offer/answer SDP that was
+     * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and
+     * the SDP string as values.
+     *
+     * @type {{}}
+     */
+    this.cache = {
+        mlB2UMap : {},
+        mlU2BMap : {}
+    };
+}
+
+module.exports = Interop;
+
+/**
+ * Changes the candidate args to match with the related Unified Plan
+ */
+Interop.prototype.candidateToUnifiedPlan = function(candidate) {
+    var cand = new RTCIceCandidate(candidate);
+
+    cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex];
+    /* TODO: change sdpMid to (audio|video)-SSRC */
+
+    return cand;
+};
+
+/**
+ * Changes the candidate args to match with the related Plan B
+ */
+Interop.prototype.candidateToPlanB = function(candidate) {
+    var cand = new RTCIceCandidate(candidate);
+
+    if (cand.sdpMid.indexOf('audio') === 0) {
+      cand.sdpMid = 'audio';
+    } else if (cand.sdpMid.indexOf('video') === 0) {
+      cand.sdpMid = 'video';
+    } else {
+      throw new Error('candidate with ' + cand.sdpMid + ' not allowed');
+    }
+
+    cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex];
+
+    return cand;
+};
+
+/**
+ * Returns the index of the first m-line with the given media type and with a
+ * direction which allows sending, in the last Unified Plan description with
+ * type "answer" converted to Plan B. Returns {null} if there is no saved
+ * answer, or if none of its m-lines with the given type allow sending.
+ * @param type the media type ("audio" or "video").
+ * @returns {*}
+ */
+Interop.prototype.getFirstSendingIndexFromAnswer = function(type) {
+    if (!this.cache.answer) {
+        return null;
+    }
+
+    var session = transform.parse(this.cache.answer);
+    if (session && session.media && Array.isArray(session.media)){
+        for (var i = 0; i < session.media.length; i++) {
+            if (session.media[i].type == type &&
+                (!session.media[i].direction /* default to sendrecv */ ||
+                    session.media[i].direction === 'sendrecv' ||
+                    session.media[i].direction === 'sendonly')){
+                return i;
+            }
+        }
+    }
+
+    return null;
+};
+
+/**
+ * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A
+ * PeerConnection wrapper transforms the SDP to Plan B before passing it to the
+ * application.
+ *
+ * @param desc
+ * @returns {*}
+ */
+Interop.prototype.toPlanB = function(desc) {
+    var self = this;
+    //#region Preliminary input validation.
+
+    if (typeof desc !== 'object' || desc === null ||
+        typeof desc.sdp !== 'string') {
+        console.warn('An empty description was passed as an argument.');
+        return desc;
+    }
+
+    // Objectify the SDP for easier manipulation.
+    var session = transform.parse(desc.sdp);
+
+    // If the SDP contains no media, there's nothing to transform.
+    if (typeof session.media === 'undefined' ||
+        !Array.isArray(session.media) || session.media.length === 0) {
+        console.warn('The description has no media.');
+        return desc;
+    }
+
+    // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B
+    // SDP has a video, an audio and a data "channel" at most.
+    if (session.media.length <= 3 && session.media.every(function(m) {
+            return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
+        })) {
+        console.warn('This description does not look like Unified Plan.');
+        return desc;
+    }
+
+    //#endregion
+
+    // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443
+    var sdp = desc.sdp;
+    var rewrite = false;
+    for (var i = 0; i < session.media.length; i++) {
+        var uLine = session.media[i];
+        uLine.rtp.forEach(function(rtp) {
+            if (rtp.codec === 'NULL')
+            {
+                rewrite = true;
+                var offer = transform.parse(self.cache.offer);
+                rtp.codec = offer.media[i].rtp[0].codec;
+            }
+        });
+    }
+    if (rewrite) {
+        sdp = transform.write(session);
+    }
+
+    // Unified Plan SDP is our "precious". Cache it for later use in the Plan B
+    // -> Unified Plan transformation.
+    this.cache[desc.type] = sdp;
+
+    //#region Convert from Unified Plan to Plan B.
+
+    // We rebuild the session.media array.
+    var media = session.media;
+    session.media = [];
+
+    // Associative array that maps channel types to channel objects for fast
+    // access to channel objects by their type, e.g. type2bl['audio']->channel
+    // obj.
+    var type2bl = {};
+
+    // Used to build the group:BUNDLE value after the channels construction
+    // loop.
+    var types = [];
+
+    media.forEach(function(uLine) {
+        // rtcp-mux is required in the Plan B SDP.
+        if ((typeof uLine.rtcpMux !== 'string' ||
+            uLine.rtcpMux !== 'rtcp-mux') &&
+            uLine.direction !== 'inactive') {
+            throw new Error('Cannot convert to Plan B because m-lines ' +
+                'without the rtcp-mux attribute were found.');
+        }
+
+        // If we don't have a channel for this uLine.type OR the selected is
+        // inactive, then select this uLine as the channel basis.
+        if (typeof type2bl[uLine.type] === 'undefined' ||
+            type2bl[uLine.type].direction === 'inactive') {
+            type2bl[uLine.type] = uLine;
+        }
+
+        if (uLine.protocol != type2bl[uLine.type].protocol) {
+          throw new Error('Cannot convert to Plan B because m-lines ' +
+              'have different protocols and this library does not have ' +
+              'support for that');
+        }
+
+        if (uLine.payloads != type2bl[uLine.type].payloads) {
+          throw new Error('Cannot convert to Plan B because m-lines ' +
+              'have different payloads and this library does not have ' +
+              'support for that');
+        }
+
+    });
+
+    // Implode the Unified Plan m-lines/tracks into Plan B channels.
+    media.forEach(function(uLine) {
+        if (uLine.type === 'application') {
+            session.media.push(uLine);
+            types.push(uLine.mid);
+            return;
+        }
+
+        // Add sources to the channel and handle a=msid.
+        if (typeof uLine.sources === 'object') {
+            Object.keys(uLine.sources).forEach(function(ssrc) {
+                if (typeof type2bl[uLine.type].sources !== 'object')
+                    type2bl[uLine.type].sources = {};
+
+                // Assign the sources to the channel.
+                type2bl[uLine.type].sources[ssrc] =
+                    uLine.sources[ssrc];
+
+                if (typeof uLine.msid !== 'undefined') {
+                    // In Plan B the msid is an SSRC attribute. Also, we don't
+                    // care about the obsolete label and mslabel attributes.
+                    //
+                    // Note that it is not guaranteed that the uLine will
+                    // have an msid. recvonly channels in particular don't have
+                    // one.
+                    type2bl[uLine.type].sources[ssrc].msid =
+                        uLine.msid;
+                }
+                // NOTE ssrcs in ssrc groups will share msids, as
+                // draft-uberti-rtcweb-plan-00 mandates.
+            });
+        }
+
+        // Add ssrc groups to the channel.
+        if (typeof uLine.ssrcGroups !== 'undefined' &&
+                Array.isArray(uLine.ssrcGroups)) {
+
+            // Create the ssrcGroups array, if it's not defined.
+            if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' ||
+                    !Array.isArray(type2bl[uLine.type].ssrcGroups)) {
+                type2bl[uLine.type].ssrcGroups = [];
+            }
+
+            type2bl[uLine.type].ssrcGroups =
+                type2bl[uLine.type].ssrcGroups.concat(
+                    uLine.ssrcGroups);
+        }
+
+        if (type2bl[uLine.type] === uLine) {
+            // Plan B mids are in ['audio', 'video', 'data']
+            uLine.mid = uLine.type;
+
+            // Plan B doesn't support/need the bundle-only attribute.
+            delete uLine.bundleOnly;
+
+            // In Plan B the msid is an SSRC attribute.
+            delete uLine.msid;
+
+	    if (uLine.type == media[0].type) {
+	      types.unshift(uLine.type);
+	      // Add the channel to the new media array.
+	      session.media.unshift(uLine);
+	    } else {
+	      types.push(uLine.type);
+	      // Add the channel to the new media array.
+	      session.media.push(uLine);
+	    }
+        }
+    });
+
+    if (typeof session.groups !== 'undefined') {
+      // We regenerate the BUNDLE group with the new mids.
+      session.groups.some(function(group) {
+	  if (group.type === 'BUNDLE') {
+	      group.mids = types.join(' ');
+	      return true;
+	  }
+      });
+    }
+
+    // msid semantic
+    session.msidSemantic = {
+        semantic: 'WMS',
+        token: '*'
+    };
+
+    var resStr = transform.write(session);
+
+    return new RTCSessionDescription({
+        type: desc.type,
+        sdp: resStr
+    });
+
+    //#endregion
+};
+
+/* follow rules defined in RFC4145 */
+function addSetupAttr(uLine) {
+    if (typeof uLine.setup === 'undefined') {
+        return;
+    }
+
+    if (uLine.setup === "active") {
+            uLine.setup = "passive";
+    } else if (uLine.setup === "passive") {
+        uLine.setup = "active";
+    }
+}
+
+/**
+ * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A
+ * PeerConnection wrapper transforms the SDP to Unified Plan before passing it
+ * to FF.
+ *
+ * @param desc
+ * @returns {*}
+ */
+Interop.prototype.toUnifiedPlan = function(desc) {
+    var self = this;
+    //#region Preliminary input validation.
+
+    if (typeof desc !== 'object' || desc === null ||
+        typeof desc.sdp !== 'string') {
+        console.warn('An empty description was passed as an argument.');
+        return desc;
+    }
+
+    var session = transform.parse(desc.sdp);
+
+    // If the SDP contains no media, there's nothing to transform.
+    if (typeof session.media === 'undefined' ||
+        !Array.isArray(session.media) || session.media.length === 0) {
+        console.warn('The description has no media.');
+        return desc;
+    }
+
+    // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has
+    // a video, an audio and a data "channel" at most.
+    if (session.media.length > 3 || !session.media.every(function(m) {
+            return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
+        })) {
+        console.warn('This description does not look like Plan B.');
+        return desc;
+    }
+
+    // Make sure this Plan B SDP can be converted to a Unified Plan SDP.
+    var mids = [];
+    session.media.forEach(function(m) {
+        mids.push(m.mid);
+    });
+
+    var hasBundle = false;
+    if (typeof session.groups !== 'undefined' &&
+        Array.isArray(session.groups)) {
+        hasBundle = session.groups.every(function(g) {
+            return g.type !== 'BUNDLE' ||
+                arrayEquals.apply(g.mids.sort(), [mids.sort()]);
+        });
+    }
+
+    if (!hasBundle) {
+        var mustBeBundle = false;
+
+        session.media.forEach(function(m) {
+            if (m.direction !== 'inactive') {
+                mustBeBundle = true;
+            }
+        });
+
+        if (mustBeBundle) {
+            throw new Error("Cannot convert to Unified Plan because m-lines that" +
+              " are not bundled were found.");
+        }
+    }
+
+    //#endregion
+
+
+    //#region Convert from Plan B to Unified Plan.
+
+    // Unfortunately, a Plan B offer/answer doesn't have enough information to
+    // rebuild an equivalent Unified Plan offer/answer.
+    //
+    // For example, if this is a local answer (in Unified Plan style) that we
+    // convert to Plan B prior to handing it over to the application (the
+    // PeerConnection wrapper called us, for instance, after a successful
+    // createAnswer), we want to remember the m-line at which we've seen the
+    // (local) SSRC. That's because when the application wants to do call the
+    // SLD method, forcing us to do the inverse transformation (from Plan B to
+    // Unified Plan), we need to know to which m-line to assign the (local)
+    // SSRC. We also need to know all the other m-lines that the original
+    // answer had and include them in the transformed answer as well.
+    //
+    // Another example is if this is a remote offer that we convert to Plan B
+    // prior to giving it to the application, we want to remember the mid at
+    // which we've seen the (remote) SSRC.
+    //
+    // In the iteration that follows, we use the cached Unified Plan (if it
+    // exists) to assign mids to ssrcs.
+
+    var type;
+    if (desc.type === 'answer') {
+        type = 'offer';
+    } else if (desc.type === 'offer') {
+        type = 'answer';
+    } else {
+        throw new Error("Type '" + desc.type + "' not supported.");
+    }
+
+    var cached;
+    if (typeof this.cache[type] !== 'undefined') {
+        cached = transform.parse(this.cache[type]);
+    }
+
+    var recvonlySsrcs = {
+        audio: {},
+        video: {}
+    };
+
+    // A helper map that sends mids to m-line objects. We use it later to
+    // rebuild the Unified Plan style session.media array.
+    var mid2ul = {};
+    var bIdx = 0;
+    var uIdx = 0;
+
+    var sources2ul = {};
+
+    var candidates;
+    var iceUfrag;
+    var icePwd;
+    var fingerprint;
+    var payloads = {};
+    var rtcpFb = {};
+    var rtp = {};
+
+    session.media.forEach(function(bLine) {
+        if ((typeof bLine.rtcpMux !== 'string' ||
+            bLine.rtcpMux !== 'rtcp-mux') &&
+            bLine.direction !== 'inactive') {
+            throw new Error("Cannot convert to Unified Plan because m-lines " +
+                "without the rtcp-mux attribute were found.");
+        }
+
+        if (bLine.type === 'application') {
+            mid2ul[bLine.mid] = bLine;
+            return;
+        }
+
+        // With rtcp-mux and bundle all the channels should have the same ICE
+        // stuff.
+        var sources = bLine.sources;
+        var ssrcGroups = bLine.ssrcGroups;
+        var port = bLine.port;
+
+        /* Chrome adds different candidates even using bundle, so we concat the candidates list */
+        if (typeof bLine.candidates != 'undefined') {
+            if (typeof candidates != 'undefined') {
+                candidates = candidates.concat(bLine.candidates);
+            } else {
+                candidates = bLine.candidates;
+            }
+        }
+
+        if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) {
+            throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" +
+                            "\tLast iceUfrag: " + iceUfrag + "\n" +
+                            "\tNew iceUfrag: " + bLine.iceUfrag
+            );
+        }
+
+        if (typeof bLine.iceUfrag != 'undefined') {
+                iceUfrag = bLine.iceUfrag;
+        }
+
+        if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) {
+            throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" +
+                            "\tLast icePwd: " + icePwd + "\n" +
+                            "\tNew icePwd: " + bLine.icePwd
+            );
+        }
+
+        if (typeof bLine.icePwd != 'undefined') {
+                icePwd = bLine.icePwd;
+        }
+
+        if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') &&
+            (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) {
+            throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" +
+                            "\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" +
+                            "\tNew fingerprint: " + JSON.stringify(bLine.fingerprint)
+            );
+        }
+
+        if (typeof bLine.fingerprint != 'undefined') {
+                fingerprint = bLine.fingerprint;
+        }
+
+        payloads[bLine.type] = bLine.payloads;
+        rtcpFb[bLine.type] = bLine.rtcpFb;
+        rtp[bLine.type] = bLine.rtp;
+
+        // inverted ssrc group map
+        var ssrc2group = {};
+        if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
+            ssrcGroups.forEach(function (ssrcGroup) {
+                // XXX This might brake if an SSRC is in more than one group
+                // for some reason.
+                if (typeof ssrcGroup.ssrcs !== 'undefined' &&
+                    Array.isArray(ssrcGroup.ssrcs)) {
+                    ssrcGroup.ssrcs.forEach(function (ssrc) {
+                        if (typeof ssrc2group[ssrc] === 'undefined') {
+                            ssrc2group[ssrc] = [];
+                        }
+
+                        ssrc2group[ssrc].push(ssrcGroup);
+                    });
+                }
+            });
+        }
+
+        // ssrc to m-line index.
+        var ssrc2ml = {};
+
+        if (typeof sources === 'object') {
+
+            // We'll use the "bLine" object as a prototype for each new "mLine"
+            // that we create, but first we need to clean it up a bit.
+            delete bLine.sources;
+            delete bLine.ssrcGroups;
+            delete bLine.candidates;
+            delete bLine.iceUfrag;
+            delete bLine.icePwd;
+            delete bLine.fingerprint;
+            delete bLine.port;
+            delete bLine.mid;
+
+            // Explode the Plan B channel sources with one m-line per source.
+            Object.keys(sources).forEach(function(ssrc) {
+
+                // The (unified) m-line for this SSRC. We either create it from
+                // scratch or, if it's a grouped SSRC, we re-use a related
+                // mline. In other words, if the source is grouped with another
+                // source, put the two together in the same m-line.
+                var uLine;
+
+                // We assume here that we are the answerer in the O/A, so any
+                // offers which we translate come from the remote side, while
+                // answers are local. So the check below is to make that we
+                // handle receive-only SSRCs in a special way only if they come
+                // from the remote side.
+                if (desc.type==='offer') {
+                    // We want to detect SSRCs which are used by a remote peer
+                    // in an m-line with direction=recvonly (i.e. they are
+                    // being used for RTCP only).
+                    // This information would have gotten lost if the remote
+                    // peer used Unified Plan and their local description was
+                    // translated to Plan B. So we use the lack of an MSID
+                    // attribute to deduce a "receive only" SSRC.
+                    if (!sources[ssrc].msid) {
+                        recvonlySsrcs[bLine.type][ssrc] = sources[ssrc];
+                        // Receive-only SSRCs must not create new m-lines. We
+                        // will assign them to an existing m-line later.
+                        return;
+                    }
+                }
+
+                if (typeof ssrc2group[ssrc] !== 'undefined' &&
+                    Array.isArray(ssrc2group[ssrc])) {
+                    ssrc2group[ssrc].some(function (ssrcGroup) {
+                        // ssrcGroup.ssrcs *is* an Array, no need to check
+                        // again here.
+                        return ssrcGroup.ssrcs.some(function (related) {
+                            if (typeof ssrc2ml[related] === 'object') {
+                                uLine = ssrc2ml[related];
+                                return true;
+                            }
+                        });
+                    });
+                }
+
+                if (typeof uLine === 'object') {
+                    // the m-line already exists. Just add the source.
+                    uLine.sources[ssrc] = sources[ssrc];
+                    delete sources[ssrc].msid;
+                } else {
+                    // Use the "bLine" as a prototype for the "uLine".
+                    uLine = Object.create(bLine);
+                    ssrc2ml[ssrc] = uLine;
+
+                    if (typeof sources[ssrc].msid !== 'undefined') {
+                        // Assign the msid of the source to the m-line. Note
+                        // that it is not guaranteed that the source will have
+                        // msid. In particular "recvonly" sources don't have an
+                        // msid. Note that "recvonly" is a term only defined
+                        // for m-lines.
+                        uLine.msid = sources[ssrc].msid;
+                        delete sources[ssrc].msid;
+                    }
+
+                    // We assign one SSRC per media line.
+                    uLine.sources = {};
+                    uLine.sources[ssrc] = sources[ssrc];
+                    uLine.ssrcGroups = ssrc2group[ssrc];
+
+                    // Use the cached Unified Plan SDP (if it exists) to assign
+                    // SSRCs to mids.
+                    if (typeof cached !== 'undefined' &&
+                        typeof cached.media !== 'undefined' &&
+                        Array.isArray(cached.media)) {
+
+                        cached.media.forEach(function (m) {
+                            if (typeof m.sources === 'object') {
+                                Object.keys(m.sources).forEach(function (s) {
+                                    if (s === ssrc) {
+                                        uLine.mid = m.mid;
+                                    }
+                                });
+                            }
+                        });
+                    }
+
+                    if (typeof uLine.mid === 'undefined') {
+
+                        // If this is an SSRC that we see for the first time
+                        // assign it a new mid. This is typically the case when
+                        // this method is called to transform a remote
+                        // description for the first time or when there is a
+                        // new SSRC in the remote description because a new
+                        // peer has joined the conference. Local SSRCs should
+                        // have already been added to the map in the toPlanB
+                        // method.
+                        //
+                        // Because FF generates answers in Unified Plan style,
+                        // we MUST already have a cached answer with all the
+                        // local SSRCs mapped to some m-line/mid.
+
+                        uLine.mid = [bLine.type, '-', ssrc].join('');
+                    }
+
+                    // Include the candidates in the 1st media line.
+                    uLine.candidates = candidates;
+                    uLine.iceUfrag = iceUfrag;
+                    uLine.icePwd = icePwd;
+                    uLine.fingerprint = fingerprint;
+                    uLine.port = port;
+
+                    mid2ul[uLine.mid] = uLine;
+                    sources2ul[uIdx] = uLine.sources;
+
+                    self.cache.mlU2BMap[uIdx] = bIdx;
+                    if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
+                      self.cache.mlB2UMap[bIdx] = uIdx;
+                    }
+                    uIdx++;
+                }
+            });
+        } else {
+          var uLine = bLine;
+
+          uLine.candidates = candidates;
+          uLine.iceUfrag = iceUfrag;
+          uLine.icePwd = icePwd;
+          uLine.fingerprint = fingerprint;
+          uLine.port = port;
+
+          mid2ul[uLine.mid] = uLine;
+
+          self.cache.mlU2BMap[uIdx] = bIdx;
+          if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
+            self.cache.mlB2UMap[bIdx] = uIdx;
+          }
+        }
+
+        bIdx++;
+    });
+
+    // Rebuild the media array in the right order and add the missing mLines
+    // (missing from the Plan B SDP).
+    session.media = [];
+    mids = []; // reuse
+
+    if (desc.type === 'answer') {
+
+        // The media lines in the answer must match the media lines in the
+        // offer. The order is important too. Here we assume that Firefox is
+        // the answerer, so we merely have to use the reconstructed (unified)
+        // answer to update the cached (unified) answer accordingly.
+        //
+        // In the general case, one would have to use the cached (unified)
+        // offer to find the m-lines that are missing from the reconstructed
+        // answer, potentially grabbing them from the cached (unified) answer.
+        // One has to be careful with this approach because inactive m-lines do
+        // not always have an mid, making it tricky (impossible?) to find where
+        // exactly and which m-lines are missing from the reconstructed answer.
+
+        for (var i = 0; i < cached.media.length; i++) {
+            var uLine = cached.media[i];
+
+            delete uLine.msid;
+            delete uLine.sources;
+            delete uLine.ssrcGroups;
+
+            if (typeof sources2ul[i] === 'undefined') {
+              if (!uLine.direction
+                  || uLine.direction === 'sendrecv')
+                  uLine.direction = 'recvonly';
+              else if (uLine.direction === 'sendonly')
+                  uLine.direction = 'inactive';
+            } else {
+              if (!uLine.direction
+                  || uLine.direction === 'sendrecv')
+                  uLine.direction = 'sendrecv';
+              else if (uLine.direction === 'recvonly')
+                  uLine.direction = 'sendonly';
+            }
+
+            uLine.sources = sources2ul[i];
+            uLine.candidates = candidates;
+            uLine.iceUfrag = iceUfrag;
+            uLine.icePwd = icePwd;
+            uLine.fingerprint = fingerprint;
+
+            uLine.rtp = rtp[uLine.type];
+            uLine.payloads = payloads[uLine.type];
+            uLine.rtcpFb = rtcpFb[uLine.type];
+
+            session.media.push(uLine);
+
+            if (typeof uLine.mid === 'string') {
+                // inactive lines don't/may not have an mid.
+                mids.push(uLine.mid);
+            }
+        }
+    } else {
+
+        // SDP offer/answer (and the JSEP spec) forbids removing an m-section
+        // under any circumstances. If we are no longer interested in sending a
+        // track, we just remove the msid and ssrc attributes and set it to
+        // either a=recvonly (as the reofferer, we must use recvonly if the
+        // other side was previously sending on the m-section, but we can also
+        // leave the possibility open if it wasn't previously in use), or
+        // a=inactive.
+
+        if (typeof cached !== 'undefined' &&
+            typeof cached.media !== 'undefined' &&
+            Array.isArray(cached.media)) {
+            cached.media.forEach(function(uLine) {
+                mids.push(uLine.mid);
+                if (typeof mid2ul[uLine.mid] !== 'undefined') {
+                    session.media.push(mid2ul[uLine.mid]);
+                } else {
+                    delete uLine.msid;
+                    delete uLine.sources;
+                    delete uLine.ssrcGroups;
+
+                    if (!uLine.direction
+                        || uLine.direction === 'sendrecv') {
+                        uLine.direction = 'sendonly';
+                    }
+                    if (!uLine.direction
+                        || uLine.direction === 'recvonly') {
+                        uLine.direction = 'inactive';
+                    }
+
+                    addSetupAttr (uLine);
+                    session.media.push(uLine);
+                }
+            });
+        }
+
+        // Add all the remaining (new) m-lines of the transformed SDP.
+        Object.keys(mid2ul).forEach(function(mid) {
+            if (mids.indexOf(mid) === -1) {
+                mids.push(mid);
+                if (mid2ul[mid].direction === 'recvonly') {
+                    // This is a remote recvonly channel. Add its SSRC to the
+                    // appropriate sendrecv or sendonly channel.
+                    // TODO(gp) what if we don't have sendrecv/sendonly
+                    // channel?
+
+                    var done = false;
+
+                    session.media.some(function (uLine) {
+                        if ((uLine.direction === 'sendrecv' ||
+                            uLine.direction === 'sendonly') &&
+                            uLine.type === mid2ul[mid].type) {
+                            // mid2ul[mid] shouldn't have any ssrc-groups
+                            Object.keys(mid2ul[mid].sources).forEach(
+                                function (ssrc) {
+                                uLine.sources[ssrc] =
+                                    mid2ul[mid].sources[ssrc];
+                            });
+
+                            done = true;
+                            return true;
+                        }
+                    });
+
+                    if (!done) {
+                        session.media.push(mid2ul[mid]);
+                    }
+                } else {
+                    session.media.push(mid2ul[mid]);
+                }
+            }
+        });
+    }
+
+    // After we have constructed the Plan Unified m-lines we can figure out
+    // where (in which m-line) to place the 'recvonly SSRCs'.
+    // Note: we assume here that we are the answerer in the O/A, so any offers
+    // which we translate come from the remote side, while answers are local
+    // (and so our last local description is cached as an 'answer').
+    ["audio", "video"].forEach(function (type) {
+        if (!session || !session.media || !Array.isArray(session.media))
+            return;
+
+        var idx = null;
+        if (Object.keys(recvonlySsrcs[type]).length > 0) {
+            idx = self.getFirstSendingIndexFromAnswer(type);
+            if (idx === null){
+                // If this is the first offer we receive, we don't have a
+                // cached answer. Assume that we will be sending media using
+                // the first m-line for each media type.
+
+                for (var i = 0; i < session.media.length; i++) {
+                    if (session.media[i].type === type) {
+                        idx = i;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (idx && session.media.length > idx) {
+            var mLine = session.media[idx];
+            Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) {
+                if (mLine.sources && mLine.sources[ssrc]) {
+                    console.warn("Replacing an existing SSRC.");
+                }
+                if (!mLine.sources) {
+                    mLine.sources = {};
+                }
+
+                mLine.sources[ssrc] = recvonlySsrcs[type][ssrc];
+            });
+        }
+    });
+
+    if (typeof session.groups !== 'undefined') {
+      // We regenerate the BUNDLE group (since we regenerated the mids)
+      session.groups.some(function(group) {
+	  if (group.type === 'BUNDLE') {
+	      group.mids = mids.join(' ');
+	      return true;
+	  }
+      });
+    }
+
+    // msid semantic
+    session.msidSemantic = {
+        semantic: 'WMS',
+        token: '*'
+    };
+
+    var resStr = transform.write(session);
+
+    // Cache the transformed SDP (Unified Plan) for later re-use in this
+    // function.
+    this.cache[desc.type] = resStr;
+
+    return new RTCSessionDescription({
+        type: desc.type,
+        sdp: resStr
+    });
+
+    //#endregion
+};
+
+},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var transform = require('sdp-transform');
+
+exports.write = function(session, opts) {
+
+  if (typeof session !== 'undefined' &&
+      typeof session.media !== 'undefined' &&
+      Array.isArray(session.media)) {
+
+    session.media.forEach(function (mLine) {
+      // expand sources to ssrcs
+      if (typeof mLine.sources !== 'undefined' &&
+        Object.keys(mLine.sources).length !== 0) {
+          mLine.ssrcs = [];
+          Object.keys(mLine.sources).forEach(function (ssrc) {
+            var source = mLine.sources[ssrc];
+            Object.keys(source).forEach(function (attribute) {
+              mLine.ssrcs.push({
+                id: ssrc,
+                attribute: attribute,
+                value: source[attribute]
+              });
+            });
+          });
+          delete mLine.sources;
+        }
+
+      // join ssrcs in ssrc groups
+      if (typeof mLine.ssrcGroups !== 'undefined' &&
+        Array.isArray(mLine.ssrcGroups)) {
+          mLine.ssrcGroups.forEach(function (ssrcGroup) {
+            if (typeof ssrcGroup.ssrcs !== 'undefined' &&
+                Array.isArray(ssrcGroup.ssrcs)) {
+              ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ');
+            }
+          });
+        }
+    });
+  }
+
+  // join group mids
+  if (typeof session !== 'undefined' &&
+      typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
+
+    session.groups.forEach(function (g) {
+      if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
+        g.mids = g.mids.join(' ');
+      }
+    });
+  }
+
+  return transform.write(session, opts);
+};
+
+exports.parse = function(sdp) {
+  var session = transform.parse(sdp);
+
+  if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
+      Array.isArray(session.media)) {
+
+    session.media.forEach(function (mLine) {
+      // group sources attributes by ssrc
+      if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
+        mLine.sources = {};
+        mLine.ssrcs.forEach(function (ssrc) {
+          if (!mLine.sources[ssrc.id])
+          mLine.sources[ssrc.id] = {};
+        mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value;
+        });
+
+        delete mLine.ssrcs;
+      }
+
+      // split ssrcs in ssrc groups
+      if (typeof mLine.ssrcGroups !== 'undefined' &&
+        Array.isArray(mLine.ssrcGroups)) {
+          mLine.ssrcGroups.forEach(function (ssrcGroup) {
+            if (typeof ssrcGroup.ssrcs === 'string') {
+              ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ');
+            }
+          });
+        }
+    });
+  }
+  // split group mids
+  if (typeof session !== 'undefined' &&
+      typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
+
+    session.groups.forEach(function (g) {
+      if (typeof g.mids === 'string') {
+        g.mids = g.mids.split(' ');
+      }
+    });
+  }
+
+  return session;
+};
+
+
+},{"sdp-transform":14}],21:[function(require,module,exports){
+/**
+ * UAParser.js v0.7.17
+ * Lightweight JavaScript-based User-Agent string parser
+ * https://github.com/faisalman/ua-parser-js
+ *
+ * Copyright © 2012-2016 Faisal Salman <fyzlman@gmail.com>
+ * Dual licensed under GPLv2 & MIT
+ */
+
+(function (window, undefined) {
+
+    'use strict';
+
+    //////////////
+    // Constants
+    /////////////
+
+
+    var LIBVERSION  = '0.7.17',
+        EMPTY       = '',
+        UNKNOWN     = '?',
+        FUNC_TYPE   = 'function',
+        UNDEF_TYPE  = 'undefined',
+        OBJ_TYPE    = 'object',
+        STR_TYPE    = 'string',
+        MAJOR       = 'major', // deprecated
+        MODEL       = 'model',
+        NAME        = 'name',
+        TYPE        = 'type',
+        VENDOR      = 'vendor',
+        VERSION     = 'version',
+        ARCHITECTURE= 'architecture',
+        CONSOLE     = 'console',
+        MOBILE      = 'mobile',
+        TABLET      = 'tablet',
+        SMARTTV     = 'smarttv',
+        WEARABLE    = 'wearable',
+        EMBEDDED    = 'embedded';
+
+
+    ///////////
+    // Helper
+    //////////
+
+
+    var util = {
+        extend : function (regexes, extensions) {
+            var margedRegexes = {};
+            for (var i in regexes) {
+                if (extensions[i] && extensions[i].length % 2 === 0) {
+                    margedRegexes[i] = extensions[i].concat(regexes[i]);
+                } else {
+                    margedRegexes[i] = regexes[i];
+                }
+            }
+            return margedRegexes;
+        },
+        has : function (str1, str2) {
+          if (typeof str1 === "string") {
+            return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1;
+          } else {
+            return false;
+          }
+        },
+        lowerize : function (str) {
+            return str.toLowerCase();
+        },
+        major : function (version) {
+            return typeof(version) === STR_TYPE ? version.replace(/[^\d\.]/g,'').split(".")[0] : undefined;
+        },
+        trim : function (str) {
+          return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
+        }
+    };
+
+
+    ///////////////
+    // Map helper
+    //////////////
+
+
+    var mapper = {
+
+        rgx : function (ua, arrays) {
+
+            //var result = {},
+            var i = 0, j, k, p, q, matches, match;//, args = arguments;
+
+            /*// construct object barebones
+            for (p = 0; p < args[1].length; p++) {
+                q = args[1][p];
+                result[typeof q === OBJ_TYPE ? q[0] : q] = undefined;
+            }*/
+
+            // loop through all regexes maps
+            while (i < arrays.length && !matches) {
+
+                var regex = arrays[i],       // even sequence (0,2,4,..)
+                    props = arrays[i + 1];   // odd sequence (1,3,5,..)
+                j = k = 0;
+
+                // try matching uastring with regexes
+                while (j < regex.length && !matches) {
+
+                    matches = regex[j++].exec(ua);
+
+                    if (!!matches) {
+                        for (p = 0; p < props.length; p++) {
+                            match = matches[++k];
+                            q = props[p];
+                            // check if given property is actually array
+                            if (typeof q === OBJ_TYPE && q.length > 0) {
+                                if (q.length == 2) {
+                                    if (typeof q[1] == FUNC_TYPE) {
+                                        // assign modified match
+                                        this[q[0]] = q[1].call(this, match);
+                                    } else {
+                                        // assign given value, ignore regex match
+                                        this[q[0]] = q[1];
+                                    }
+                                } else if (q.length == 3) {
+                                    // check whether function or regex
+                                    if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
+                                        // call function (usually string mapper)
+                                        this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined;
+                                    } else {
+                                        // sanitize match using given regex
+                                        this[q[0]] = match ? match.replace(q[1], q[2]) : undefined;
+                                    }
+                                } else if (q.length == 4) {
+                                        this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined;
+                                }
+                            } else {
+                                this[q] = match ? match : undefined;
+                            }
+                        }
+                    }
+                }
+                i += 2;
+            }
+            // console.log(this);
+            //return this;
+        },
+
+        str : function (str, map) {
+
+            for (var i in map) {
+                // check if array
+                if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
+                    for (var j = 0; j < map[i].length; j++) {
+                        if (util.has(map[i][j], str)) {
+                            return (i === UNKNOWN) ? undefined : i;
+                        }
+                    }
+                } else if (util.has(map[i], str)) {
+                    return (i === UNKNOWN) ? undefined : i;
+                }
+            }
+            return str;
+        }
+    };
+
+
+    ///////////////
+    // String map
+    //////////////
+
+
+    var maps = {
+
+        browser : {
+            oldsafari : {
+                version : {
+                    '1.0'   : '/8',
+                    '1.2'   : '/1',
+                    '1.3'   : '/3',
+                    '2.0'   : '/412',
+                    '2.0.2' : '/416',
+                    '2.0.3' : '/417',
+                    '2.0.4' : '/419',
+                    '?'     : '/'
+                }
+            }
+        },
+
+        device : {
+            amazon : {
+                model : {
+                    'Fire Phone' : ['SD', 'KF']
+                }
+            },
+            sprint : {
+                model : {
+                    'Evo Shift 4G' : '7373KT'
+                },
+                vendor : {
+                    'HTC'       : 'APA',
+                    'Sprint'    : 'Sprint'
+                }
+            }
+        },
+
+        os : {
+            windows : {
+                version : {
+                    'ME'        : '4.90',
+                    'NT 3.11'   : 'NT3.51',
+                    'NT 4.0'    : 'NT4.0',
+                    '2000'      : 'NT 5.0',
+                    'XP'        : ['NT 5.1', 'NT 5.2'],
+                    'Vista'     : 'NT 6.0',
+                    '7'         : 'NT 6.1',
+                    '8'         : 'NT 6.2',
+                    '8.1'       : 'NT 6.3',
+                    '10'        : ['NT 6.4', 'NT 10.0'],
+                    'RT'        : 'ARM'
+                }
+            }
+        }
+    };
+
+
+    //////////////
+    // Regex map
+    /////////////
+
+
+    var regexes = {
+
+        browser : [[
+
+            // Presto based
+            /(opera\smini)\/([\w\.-]+)/i,                                       // Opera Mini
+            /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i,                      // Opera Mobi/Tablet
+            /(opera).+version\/([\w\.]+)/i,                                     // Opera > 9.80
+            /(opera)[\/\s]+([\w\.]+)/i                                          // Opera < 9.80
+            ], [NAME, VERSION], [
+
+            /(opios)[\/\s]+([\w\.]+)/i                                          // Opera mini on iphone >= 8.0
+            ], [[NAME, 'Opera Mini'], VERSION], [
+
+            /\s(opr)\/([\w\.]+)/i                                               // Opera Webkit
+            ], [[NAME, 'Opera'], VERSION], [
+
+            // Mixed
+            /(kindle)\/([\w\.]+)/i,                                             // Kindle
+            /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i,
+                                                                                // Lunascape/Maxthon/Netfront/Jasmine/Blazer
+
+            // Trident based
+            /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i,
+                                                                                // Avant/IEMobile/SlimBrowser/Baidu
+            /(?:ms|\()(ie)\s([\w\.]+)/i,                                        // Internet Explorer
+
+            // Webkit/KHTML based
+            /(rekonq)\/([\w\.]+)*/i,                                            // Rekonq
+            /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser)\/([\w\.-]+)/i
+                                                                                // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser
+            ], [NAME, VERSION], [
+
+            /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i                         // IE11
+            ], [[NAME, 'IE'], VERSION], [
+
+            /(edge)\/((\d+)?[\w\.]+)/i                                          // Microsoft Edge
+            ], [NAME, VERSION], [
+
+            /(yabrowser)\/([\w\.]+)/i                                           // Yandex
+            ], [[NAME, 'Yandex'], VERSION], [
+
+            /(puffin)\/([\w\.]+)/i                                              // Puffin
+            ], [[NAME, 'Puffin'], VERSION], [
+
+            /((?:[\s\/])uc?\s?browser|(?:juc.+)ucweb)[\/\s]?([\w\.]+)/i
+                                                                                // UCBrowser
+            ], [[NAME, 'UCBrowser'], VERSION], [
+
+            /(comodo_dragon)\/([\w\.]+)/i                                       // Comodo Dragon
+            ], [[NAME, /_/g, ' '], VERSION], [
+
+            /(micromessenger)\/([\w\.]+)/i                                      // WeChat
+            ], [[NAME, 'WeChat'], VERSION], [
+
+            /(QQ)\/([\d\.]+)/i                                                  // QQ, aka ShouQ
+            ], [NAME, VERSION], [
+
+            /m?(qqbrowser)[\/\s]?([\w\.]+)/i                                    // QQBrowser
+            ], [NAME, VERSION], [
+
+            /xiaomi\/miuibrowser\/([\w\.]+)/i                                   // MIUI Browser
+            ], [VERSION, [NAME, 'MIUI Browser']], [
+
+            /;fbav\/([\w\.]+);/i                                                // Facebook App for iOS & Android
+            ], [VERSION, [NAME, 'Facebook']], [
+
+            /headlesschrome(?:\/([\w\.]+)|\s)/i                                 // Chrome Headless
+            ], [VERSION, [NAME, 'Chrome Headless']], [
+
+            /\swv\).+(chrome)\/([\w\.]+)/i                                      // Chrome WebView
+            ], [[NAME, /(.+)/, '$1 WebView'], VERSION], [
+
+            /((?:oculus|samsung)browser)\/([\w\.]+)/i
+            ], [[NAME, /(.+(?:g|us))(.+)/, '$1 $2'], VERSION], [                // Oculus / Samsung Browser
+
+            /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i        // Android Browser
+            ], [VERSION, [NAME, 'Android Browser']], [
+
+            /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i
+                                                                                // Chrome/OmniWeb/Arora/Tizen/Nokia
+            ], [NAME, VERSION], [
+
+            /(dolfin)\/([\w\.]+)/i                                              // Dolphin
+            ], [[NAME, 'Dolphin'], VERSION], [
+
+            /((?:android.+)crmo|crios)\/([\w\.]+)/i                             // Chrome for Android/iOS
+            ], [[NAME, 'Chrome'], VERSION], [
+
+            /(coast)\/([\w\.]+)/i                                               // Opera Coast
+            ], [[NAME, 'Opera Coast'], VERSION], [
+
+            /fxios\/([\w\.-]+)/i                                                // Firefox for iOS
+            ], [VERSION, [NAME, 'Firefox']], [
+
+            /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i                       // Mobile Safari
+            ], [VERSION, [NAME, 'Mobile Safari']], [
+
+            /version\/([\w\.]+).+?(mobile\s?safari|safari)/i                    // Safari & Safari Mobile
+            ], [VERSION, NAME], [
+
+            /webkit.+?(gsa)\/([\w\.]+).+?(mobile\s?safari|safari)(\/[\w\.]+)/i  // Google Search Appliance on iOS
+            ], [[NAME, 'GSA'], VERSION], [
+
+            /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i                     // Safari < 3.0
+            ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [
+
+            /(konqueror)\/([\w\.]+)/i,                                          // Konqueror
+            /(webkit|khtml)\/([\w\.]+)/i
+            ], [NAME, VERSION], [
+
+            // Gecko based
+            /(navigator|netscape)\/([\w\.-]+)/i                                 // Netscape
+            ], [[NAME, 'Netscape'], VERSION], [
+            /(swiftfox)/i,                                                      // Swiftfox
+            /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,
+                                                                                // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
+            /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i,
+                                                                                // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
+            /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i,                          // Mozilla
+
+            // Other
+            /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,
+                                                                                // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir
+            /(links)\s\(([\w\.]+)/i,                                            // Links
+            /(gobrowser)\/?([\w\.]+)*/i,                                        // GoBrowser
+            /(ice\s?browser)\/v?([\w\._]+)/i,                                   // ICE Browser
+            /(mosaic)[\/\s]([\w\.]+)/i                                          // Mosaic
+            ], [NAME, VERSION]
+
+            /* /////////////////////
+            // Media players BEGIN
+            ////////////////////////
+
+            , [
+
+            /(apple(?:coremedia|))\/((\d+)[\w\._]+)/i,                          // Generic Apple CoreMedia
+            /(coremedia) v((\d+)[\w\._]+)/i
+            ], [NAME, VERSION], [
+
+            /(aqualung|lyssna|bsplayer)\/((\d+)?[\w\.-]+)/i                     // Aqualung/Lyssna/BSPlayer
+            ], [NAME, VERSION], [
+
+            /(ares|ossproxy)\s((\d+)[\w\.-]+)/i                                 // Ares/OSSProxy
+            ], [NAME, VERSION], [
+
+            /(audacious|audimusicstream|amarok|bass|core|dalvik|gnomemplayer|music on console|nsplayer|psp-internetradioplayer|videos)\/((\d+)[\w\.-]+)/i,
+                                                                                // Audacious/AudiMusicStream/Amarok/BASS/OpenCORE/Dalvik/GnomeMplayer/MoC
+                                                                                // NSPlayer/PSP-InternetRadioPlayer/Videos
+            /(clementine|music player daemon)\s((\d+)[\w\.-]+)/i,               // Clementine/MPD
+            /(lg player|nexplayer)\s((\d+)[\d\.]+)/i,
+            /player\/(nexplayer|lg player)\s((\d+)[\w\.-]+)/i                   // NexPlayer/LG Player
+            ], [NAME, VERSION], [
+            /(nexplayer)\s((\d+)[\w\.-]+)/i                                     // Nexplayer
+            ], [NAME, VERSION], [
+
+            /(flrp)\/((\d+)[\w\.-]+)/i                                          // Flip Player
+            ], [[NAME, 'Flip Player'], VERSION], [
+
+            /(fstream|nativehost|queryseekspider|ia-archiver|facebookexternalhit)/i
+                                                                                // FStream/NativeHost/QuerySeekSpider/IA Archiver/facebookexternalhit
+            ], [NAME], [
+
+            /(gstreamer) souphttpsrc (?:\([^\)]+\)){0,1} libsoup\/((\d+)[\w\.-]+)/i
+                                                                                // Gstreamer
+            ], [NAME, VERSION], [
+
+            /(htc streaming player)\s[\w_]+\s\/\s((\d+)[\d\.]+)/i,              // HTC Streaming Player
+            /(java|python-urllib|python-requests|wget|libcurl)\/((\d+)[\w\.-_]+)/i,
+                                                                                // Java/urllib/requests/wget/cURL
+            /(lavf)((\d+)[\d\.]+)/i                                             // Lavf (FFMPEG)
+            ], [NAME, VERSION], [
+
+            /(htc_one_s)\/((\d+)[\d\.]+)/i                                      // HTC One S
+            ], [[NAME, /_/g, ' '], VERSION], [
+
+            /(mplayer)(?:\s|\/)(?:(?:sherpya-){0,1}svn)(?:-|\s)(r\d+(?:-\d+[\w\.-]+){0,1})/i
+                                                                                // MPlayer SVN
+            ], [NAME, VERSION], [
+
+            /(mplayer)(?:\s|\/|[unkow-]+)((\d+)[\w\.-]+)/i                      // MPlayer
+            ], [NAME, VERSION], [
+
+            /(mplayer)/i,                                                       // MPlayer (no other info)
+            /(yourmuze)/i,                                                      // YourMuze
+            /(media player classic|nero showtime)/i                             // Media Player Classic/Nero ShowTime
+            ], [NAME], [
+
+            /(nero (?:home|scout))\/((\d+)[\w\.-]+)/i                           // Nero Home/Nero Scout
+            ], [NAME, VERSION], [
+
+            /(nokia\d+)\/((\d+)[\w\.-]+)/i                                      // Nokia
+            ], [NAME, VERSION], [
+
+            /\s(songbird)\/((\d+)[\w\.-]+)/i                                    // Songbird/Philips-Songbird
+            ], [NAME, VERSION], [
+
+            /(winamp)3 version ((\d+)[\w\.-]+)/i,                               // Winamp
+            /(winamp)\s((\d+)[\w\.-]+)/i,
+            /(winamp)mpeg\/((\d+)[\w\.-]+)/i
+            ], [NAME, VERSION], [
+
+            /(ocms-bot|tapinradio|tunein radio|unknown|winamp|inlight radio)/i  // OCMS-bot/tap in radio/tunein/unknown/winamp (no other info)
+                                                                                // inlight radio
+            ], [NAME], [
+
+            /(quicktime|rma|radioapp|radioclientapplication|soundtap|totem|stagefright|streamium)\/((\d+)[\w\.-]+)/i
+                                                                                // QuickTime/RealMedia/RadioApp/RadioClientApplication/
+                                                                                // SoundTap/Totem/Stagefright/Streamium
+            ], [NAME, VERSION], [
+
+            /(smp)((\d+)[\d\.]+)/i                                              // SMP
+            ], [NAME, VERSION], [
+
+            /(vlc) media player - version ((\d+)[\w\.]+)/i,                     // VLC Videolan
+            /(vlc)\/((\d+)[\w\.-]+)/i,
+            /(xbmc|gvfs|xine|xmms|irapp)\/((\d+)[\w\.-]+)/i,                    // XBMC/gvfs/Xine/XMMS/irapp
+            /(foobar2000)\/((\d+)[\d\.]+)/i,                                    // Foobar2000
+            /(itunes)\/((\d+)[\d\.]+)/i                                         // iTunes
+            ], [NAME, VERSION], [
+
+            /(wmplayer)\/((\d+)[\w\.-]+)/i,                                     // Windows Media Player
+            /(windows-media-player)\/((\d+)[\w\.-]+)/i
+            ], [[NAME, /-/g, ' '], VERSION], [
+
+            /windows\/((\d+)[\w\.-]+) upnp\/[\d\.]+ dlnadoc\/[\d\.]+ (home media server)/i
+                                                                                // Windows Media Server
+            ], [VERSION, [NAME, 'Windows']], [
+
+            /(com\.riseupradioalarm)\/((\d+)[\d\.]*)/i                          // RiseUP Radio Alarm
+            ], [NAME, VERSION], [
+
+            /(rad.io)\s((\d+)[\d\.]+)/i,                                        // Rad.io
+            /(radio.(?:de|at|fr))\s((\d+)[\d\.]+)/i
+            ], [[NAME, 'rad.io'], VERSION]
+
+            //////////////////////
+            // Media players END
+            ////////////////////*/
+
+        ],
+
+        cpu : [[
+
+            /(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i                     // AMD64
+            ], [[ARCHITECTURE, 'amd64']], [
+
+            /(ia32(?=;))/i                                                      // IA32 (quicktime)
+            ], [[ARCHITECTURE, util.lowerize]], [
+
+            /((?:i[346]|x)86)[;\)]/i                                            // IA32
+            ], [[ARCHITECTURE, 'ia32']], [
+
+            // PocketPC mistakenly identified as PowerPC
+            /windows\s(ce|mobile);\sppc;/i
+            ], [[ARCHITECTURE, 'arm']], [
+
+            /((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i                           // PowerPC
+            ], [[ARCHITECTURE, /ower/, '', util.lowerize]], [
+
+            /(sun4\w)[;\)]/i                                                    // SPARC
+            ], [[ARCHITECTURE, 'sparc']], [
+
+            /((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i
+                                                                                // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
+            ], [[ARCHITECTURE, util.lowerize]]
+        ],
+
+        device : [[
+
+            /\((ipad|playbook);[\w\s\);-]+(rim|apple)/i                         // iPad/PlayBook
+            ], [MODEL, VENDOR, [TYPE, TABLET]], [
+
+            /applecoremedia\/[\w\.]+ \((ipad)/                                  // iPad
+            ], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [
+
+            /(apple\s{0,1}tv)/i                                                 // Apple TV
+            ], [[MODEL, 'Apple TV'], [VENDOR, 'Apple']], [
+
+            /(archos)\s(gamepad2?)/i,                                           // Archos
+            /(hp).+(touchpad)/i,                                                // HP TouchPad
+            /(hp).+(tablet)/i,                                                  // HP Tablet
+            /(kindle)\/([\w\.]+)/i,                                             // Kindle
+            /\s(nook)[\w\s]+build\/(\w+)/i,                                     // Nook
+            /(dell)\s(strea[kpr\s\d]*[\dko])/i                                  // Dell Streak
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i                               // Kindle Fire HD
+            ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
+            /(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i                  // Fire Phone
+            ], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [
+
+            /\((ip[honed|\s\w*]+);.+(apple)/i                                   // iPod/iPhone
+            ], [MODEL, VENDOR, [TYPE, MOBILE]], [
+            /\((ip[honed|\s\w*]+);/i                                            // iPod/iPhone
+            ], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [
+
+            /(blackberry)[\s-]?(\w+)/i,                                         // BlackBerry
+            /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i,
+                                                                                // BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron
+            /(hp)\s([\w\s]+\w)/i,                                               // HP iPAQ
+            /(asus)-?(\w+)/i                                                    // Asus
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /\(bb10;\s(\w+)/i                                                   // BlackBerry 10
+            ], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [
+                                                                                // Asus Tablets
+            /android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone)/i
+            ], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [
+
+            /(sony)\s(tablet\s[ps])\sbuild\//i,                                  // Sony
+            /(sony)?(?:sgp.+)\sbuild\//i
+            ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [
+            /android.+\s([c-g]\d{4}|so[-l]\w+)\sbuild\//i
+            ], [MODEL, [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+
+            /\s(ouya)\s/i,                                                      // Ouya
+            /(nintendo)\s([wids3u]+)/i                                          // Nintendo
+            ], [VENDOR, MODEL, [TYPE, CONSOLE]], [
+
+            /android.+;\s(shield)\sbuild/i                                      // Nvidia
+            ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
+
+            /(playstation\s[34portablevi]+)/i                                   // Playstation
+            ], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [
+
+            /(sprint\s(\w+))/i                                                  // Sprint Phones
+            ], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [
+
+            /(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i                         // Lenovo tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i,                               // HTC
+            /(zte)-(\w+)*/i,                                                    // ZTE
+            /(alcatel|geeksphone|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i
+                                                                                // Alcatel/GeeksPhone/Lenovo/Nexian/Panasonic/Sony
+            ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [
+
+            /(nexus\s9)/i                                                       // HTC Nexus 9
+            ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
+
+            /d\/huawei([\w\s-]+)[;\)]/i,
+            /(nexus\s6p)/i                                                      // Huawei
+            ], [MODEL, [VENDOR, 'Huawei'], [TYPE, MOBILE]], [
+
+            /(microsoft);\s(lumia[\s\w]+)/i                                     // Microsoft Lumia
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /[\s\(;](xbox(?:\sone)?)[\s\);]/i                                   // Microsoft Xbox
+            ], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [
+            /(kin\.[onetw]{3})/i                                                // Microsoft Kin
+            ], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [
+
+                                                                                // Motorola
+            /\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i,
+            /mot[\s-]?(\w+)*/i,
+            /(XT\d{3,4}) build\//i,
+            /(nexus\s6)/i
+            ], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [
+            /android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i
+            ], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [
+
+            /hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i            // HbbTV devices
+            ], [[VENDOR, util.trim], [MODEL, util.trim], [TYPE, SMARTTV]], [
+
+            /hbbtv.+maple;(\d+)/i
+            ], [[MODEL, /^/, 'SmartTV'], [VENDOR, 'Samsung'], [TYPE, SMARTTV]], [
+
+            /\(dtv[\);].+(aquos)/i                                              // Sharp
+            ], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [
+
+            /android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,
+            /((SM-T\w+))/i
+            ], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [                  // Samsung
+            /smart-tv.+(samsung)/i
+            ], [VENDOR, [TYPE, SMARTTV], MODEL], [
+            /((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,
+            /(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i,
+            /sec-((sgh\w+))/i
+            ], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [
+
+            /sie-(\w+)*/i                                                       // Siemens
+            ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
+
+            /(maemo|nokia).*(n900|lumia\s\d+)/i,                                // Nokia
+            /(nokia)[\s_-]?([\w-]+)*/i
+            ], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [
+
+            /android\s3\.[\s\w;-]{10}(a\d{3})/i                                 // Acer
+            ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [
+
+            /android.+([vl]k\-?\d{3})\s+build/i                                 // LG Tablet
+            ], [MODEL, [VENDOR, 'LG'], [TYPE, TABLET]], [
+            /android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i                     // LG Tablet
+            ], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [
+            /(lg) netcast\.tv/i                                                 // LG SmartTV
+            ], [VENDOR, MODEL, [TYPE, SMARTTV]], [
+            /(nexus\s[45])/i,                                                   // LG
+            /lg[e;\s\/-]+(\w+)*/i,
+            /android.+lg(\-?[\d\w]+)\s+build/i
+            ], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [
+
+            /android.+(ideatab[a-z0-9\-\s]+)/i                                  // Lenovo
+            ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [
+
+            /linux;.+((jolla));/i                                               // Jolla
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /((pebble))app\/[\d\.]+\s/i                                         // Pebble
+            ], [VENDOR, MODEL, [TYPE, WEARABLE]], [
+
+            /android.+;\s(oppo)\s?([\w\s]+)\sbuild/i                            // OPPO
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /crkey/i                                                            // Google Chromecast
+            ], [[MODEL, 'Chromecast'], [VENDOR, 'Google']], [
+
+            /android.+;\s(glass)\s\d/i                                          // Google Glass
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [
+
+            /android.+;\s(pixel c)\s/i                                          // Google Pixel C
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, TABLET]], [
+
+            /android.+;\s(pixel xl|pixel)\s/i                                   // Google Pixel
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, MOBILE]], [
+
+            /android.+(\w+)\s+build\/hm\1/i,                                    // Xiaomi Hongmi 'numeric' models
+            /android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i,               // Xiaomi Hongmi
+            /android.+(mi[\s\-_]*(?:one|one[\s_]plus|note lte)?[\s_]*(?:\d\w)?)\s+build/i,    // Xiaomi Mi
+            /android.+(redmi[\s\-_]*(?:note)?(?:[\s_]*[\w\s]+)?)\s+build/i      // Redmi Phones
+            ], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [
+            /android.+(mi[\s\-_]*(?:pad)?(?:[\s_]*[\w\s]+)?)\s+build/i          // Mi Pad tablets
+            ],[[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, TABLET]], [
+            /android.+;\s(m[1-5]\snote)\sbuild/i                                // Meizu Tablet
+            ], [MODEL, [VENDOR, 'Meizu'], [TYPE, TABLET]], [
+
+            /android.+a000(1)\s+build/i                                         // OnePlus
+            ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(RCT[\d\w]+)\s+build/i                            // RCA Tablets
+            ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Venue[\d\s]*)\s+build/i                          // Dell Venue Tablets
+            ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Q[T|M][\d\w]+)\s+build/i                         // Verizon Tablet
+            ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s+(Barnes[&\s]+Noble\s+|BN[RT])(V?.*)\s+build/i     // Barnes & Noble Tablet
+            ], [[VENDOR, 'Barnes & Noble'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s+(TM\d{3}.*\b)\s+build/i                           // Barnes & Noble Tablet
+            ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(zte)?.+(k\d{2})\s+build/i                        // ZTE K Series Tablet
+            ], [[VENDOR, 'ZTE'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(gen\d{3})\s+build.*49h/i                         // Swiss GEN Mobile
+            ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(zur\d{3})\s+build/i                              // Swiss ZUR Tablet
+            ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*((Zeki)?TB.*\b)\s+build/i                         // Zeki Tablets
+            ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [
+
+            /(android).+[;\/]\s+([YR]\d{2}x?.*)\s+build/i,
+            /android.+[;\/]\s+(Dragon[\-\s]+Touch\s+|DT)(.+)\s+build/i          // Dragon Touch Tablet
+            ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(NS-?.+)\s+build/i                                // Insignia Tablets
+            ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*((NX|Next)-?.+)\s+build/i                         // NextBook Tablets
+            ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Xtreme\_?)?(V(1[045]|2[015]|30|40|60|7[05]|90))\s+build/i
+            ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [                    // Voice Xtreme Phones
+
+            /android.+[;\/]\s*(LVTEL\-?)?(V1[12])\s+build/i                     // LvTel Phones
+            ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(V(100MD|700NA|7011|917G).*\b)\s+build/i          // Envizen Tablets
+            ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Le[\s\-]+Pan)[\s\-]+(.*\b)\s+build/i             // Le Pan Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Trio[\s\-]*.*)\s+build/i                         // MachSpeed Tablets
+            ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Trinity)[\-\s]*(T\d{3})\s+build/i                // Trinity Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*TU_(1491)\s+build/i                               // Rotor Tablets
+            ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [
+
+            /android.+(KS(.+))\s+build/i                                        // Amazon Kindle Tablets
+            ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
+
+            /android.+(Gigaset)[\s\-]+(Q.+)\s+build/i                           // Gigaset Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /\s(tablet|tab)[;\/]/i,                                             // Unidentifiable Tablet
+            /\s(mobile)(?:[;\/]|\ssafari)/i                                     // Unidentifiable Mobile
+            ], [[TYPE, util.lowerize], VENDOR, MODEL], [
+
+            /(android.+)[;\/].+build/i                                          // Generic Android Device
+            ], [MODEL, [VENDOR, 'Generic']]
+
+
+        /*//////////////////////////
+            // TODO: move to string map
+            ////////////////////////////
+
+            /(C6603)/i                                                          // Sony Xperia Z C6603
+            ], [[MODEL, 'Xperia Z C6603'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+            /(C6903)/i                                                          // Sony Xperia Z 1
+            ], [[MODEL, 'Xperia Z 1'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+
+            /(SM-G900[F|H])/i                                                   // Samsung Galaxy S5
+            ], [[MODEL, 'Galaxy S5'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G7102)/i                                                       // Samsung Galaxy Grand 2
+            ], [[MODEL, 'Galaxy Grand 2'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G530H)/i                                                       // Samsung Galaxy Grand Prime
+            ], [[MODEL, 'Galaxy Grand Prime'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G313HZ)/i                                                      // Samsung Galaxy V
+            ], [[MODEL, 'Galaxy V'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-T805)/i                                                        // Samsung Galaxy Tab S 10.5
+            ], [[MODEL, 'Galaxy Tab S 10.5'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
+            /(SM-G800F)/i                                                       // Samsung Galaxy S5 Mini
+            ], [[MODEL, 'Galaxy S5 Mini'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-T311)/i                                                        // Samsung Galaxy Tab 3 8.0
+            ], [[MODEL, 'Galaxy Tab 3 8.0'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
+
+            /(T3C)/i                                                            // Advan Vandroid T3C
+            ], [MODEL, [VENDOR, 'Advan'], [TYPE, TABLET]], [
+            /(ADVAN T1J\+)/i                                                    // Advan Vandroid T1J+
+            ], [[MODEL, 'Vandroid T1J+'], [VENDOR, 'Advan'], [TYPE, TABLET]], [
+            /(ADVAN S4A)/i                                                      // Advan Vandroid S4A
+            ], [[MODEL, 'Vandroid S4A'], [VENDOR, 'Advan'], [TYPE, MOBILE]], [
+
+            /(V972M)/i                                                          // ZTE V972M
+            ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [
+
+            /(i-mobile)\s(IQ\s[\d\.]+)/i                                        // i-mobile IQ
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /(IQ6.3)/i                                                          // i-mobile IQ IQ 6.3
+            ], [[MODEL, 'IQ 6.3'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
+            /(i-mobile)\s(i-style\s[\d\.]+)/i                                   // i-mobile i-STYLE
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /(i-STYLE2.1)/i                                                     // i-mobile i-STYLE 2.1
+            ], [[MODEL, 'i-STYLE 2.1'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
+
+            /(mobiistar touch LAI 512)/i                                        // mobiistar touch LAI 512
+            ], [[MODEL, 'Touch LAI 512'], [VENDOR, 'mobiistar'], [TYPE, MOBILE]], [
+
+            /////////////
+            // END TODO
+            ///////////*/
+
+        ],
+
+        engine : [[
+
+            /windows.+\sedge\/([\w\.]+)/i                                       // EdgeHTML
+            ], [VERSION, [NAME, 'EdgeHTML']], [
+
+            /(presto)\/([\w\.]+)/i,                                             // Presto
+            /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i,     // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m
+            /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i,                          // KHTML/Tasman/Links
+            /(icab)[\/\s]([23]\.[\d\.]+)/i                                      // iCab
+            ], [NAME, VERSION], [
+
+            /rv\:([\w\.]+).*(gecko)/i                                           // Gecko
+            ], [VERSION, NAME]
+        ],
+
+        os : [[
+
+            // Windows based
+            /microsoft\s(windows)\s(vista|xp)/i                                 // Windows (iTunes)
+            ], [NAME, VERSION], [
+            /(windows)\snt\s6\.2;\s(arm)/i,                                     // Windows RT
+            /(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s]+\w)*/i,                  // Windows Phone
+            /(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i
+            ], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [
+            /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i
+            ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [
+
+            // Mobile/Embedded OS
+            /\((bb)(10);/i                                                      // BlackBerry 10
+            ], [[NAME, 'BlackBerry'], VERSION], [
+            /(blackberry)\w*\/?([\w\.]+)*/i,                                    // Blackberry
+            /(tizen)[\/\s]([\w\.]+)/i,                                          // Tizen
+            /(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i,
+                                                                                // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki
+            /linux;.+(sailfish);/i                                              // Sailfish OS
+            ], [NAME, VERSION], [
+            /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i                 // Symbian
+            ], [[NAME, 'Symbian'], VERSION], [
+            /\((series40);/i                                                    // Series 40
+            ], [NAME], [
+            /mozilla.+\(mobile;.+gecko.+firefox/i                               // Firefox OS
+            ], [[NAME, 'Firefox OS'], VERSION], [
+
+            // Console
+            /(nintendo|playstation)\s([wids34portablevu]+)/i,                   // Nintendo/Playstation
+
+            // GNU/Linux based
+            /(mint)[\/\s\(]?(\w+)*/i,                                           // Mint
+            /(mageia|vectorlinux)[;\s]/i,                                       // Mageia/VectorLinux
+            /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]+)*/i,
+                                                                                // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware
+                                                                                // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus
+            /(hurd|linux)\s?([\w\.]+)*/i,                                       // Hurd/Linux
+            /(gnu)\s?([\w\.]+)*/i                                               // GNU
+            ], [NAME, VERSION], [
+
+            /(cros)\s[\w]+\s([\w\.]+\w)/i                                       // Chromium OS
+            ], [[NAME, 'Chromium OS'], VERSION],[
+
+            // Solaris
+            /(sunos)\s?([\w\.]+\d)*/i                                           // Solaris
+            ], [[NAME, 'Solaris'], VERSION], [
+
+            // BSD based
+            /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i                   // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly
+            ], [NAME, VERSION],[
+
+            /(haiku)\s(\w+)/i                                                  // Haiku
+            ], [NAME, VERSION],[
+
+            /cfnetwork\/.+darwin/i,
+            /ip[honead]+(?:.*os\s([\w]+)\slike\smac|;\sopera)/i                 // iOS
+            ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
+
+            /(mac\sos\sx)\s?([\w\s\.]+\w)*/i,
+            /(macintosh|mac(?=_powerpc)\s)/i                                    // Mac OS
+            ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [
+
+            // Other
+            /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i,                            // Solaris
+            /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i,                               // AIX
+            /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i,
+                                                                                // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS
+            /(unix)\s?([\w\.]+)*/i                                              // UNIX
+            ], [NAME, VERSION]
+        ]
+    };
+
+
+    /////////////////
+    // Constructor
+    ////////////////
+    /*
+    var Browser = function (name, version) {
+        this[NAME] = name;
+        this[VERSION] = version;
+    };
+    var CPU = function (arch) {
+        this[ARCHITECTURE] = arch;
+    };
+    var Device = function (vendor, model, type) {
+        this[VENDOR] = vendor;
+        this[MODEL] = model;
+        this[TYPE] = type;
+    };
+    var Engine = Browser;
+    var OS = Browser;
+    */
+    var UAParser = function (uastring, extensions) {
+
+        if (typeof uastring === 'object') {
+            extensions = uastring;
+            uastring = undefined;
+        }
+
+        if (!(this instanceof UAParser)) {
+            return new UAParser(uastring, extensions).getResult();
+        }
+
+        var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY);
+        var rgxmap = extensions ? util.extend(regexes, extensions) : regexes;
+        //var browser = new Browser();
+        //var cpu = new CPU();
+        //var device = new Device();
+        //var engine = new Engine();
+        //var os = new OS();
+
+        this.getBrowser = function () {
+            var browser = { name: undefined, version: undefined };
+            mapper.rgx.call(browser, ua, rgxmap.browser);
+            browser.major = util.major(browser.version); // deprecated
+            return browser;
+        };
+        this.getCPU = function () {
+            var cpu = { architecture: undefined };
+            mapper.rgx.call(cpu, ua, rgxmap.cpu);
+            return cpu;
+        };
+        this.getDevice = function () {
+            var device = { vendor: undefined, model: undefined, type: undefined };
+            mapper.rgx.call(device, ua, rgxmap.device);
+            return device;
+        };
+        this.getEngine = function () {
+            var engine = { name: undefined, version: undefined };
+            mapper.rgx.call(engine, ua, rgxmap.engine);
+            return engine;
+        };
+        this.getOS = function () {
+            var os = { name: undefined, version: undefined };
+            mapper.rgx.call(os, ua, rgxmap.os);
+            return os;
+        };
+        this.getResult = function () {
+            return {
+                ua      : this.getUA(),
+                browser : this.getBrowser(),
+                engine  : this.getEngine(),
+                os      : this.getOS(),
+                device  : this.getDevice(),
+                cpu     : this.getCPU()
+            };
+        };
+        this.getUA = function () {
+            return ua;
+        };
+        this.setUA = function (uastring) {
+            ua = uastring;
+            //browser = new Browser();
+            //cpu = new CPU();
+            //device = new Device();
+            //engine = new Engine();
+            //os = new OS();
+            return this;
+        };
+        return this;
+    };
+
+    UAParser.VERSION = LIBVERSION;
+    UAParser.BROWSER = {
+        NAME    : NAME,
+        MAJOR   : MAJOR, // deprecated
+        VERSION : VERSION
+    };
+    UAParser.CPU = {
+        ARCHITECTURE : ARCHITECTURE
+    };
+    UAParser.DEVICE = {
+        MODEL   : MODEL,
+        VENDOR  : VENDOR,
+        TYPE    : TYPE,
+        CONSOLE : CONSOLE,
+        MOBILE  : MOBILE,
+        SMARTTV : SMARTTV,
+        TABLET  : TABLET,
+        WEARABLE: WEARABLE,
+        EMBEDDED: EMBEDDED
+    };
+    UAParser.ENGINE = {
+        NAME    : NAME,
+        VERSION : VERSION
+    };
+    UAParser.OS = {
+        NAME    : NAME,
+        VERSION : VERSION
+    };
+    //UAParser.Utils = util;
+
+    ///////////
+    // Export
+    //////////
+
+
+    // check js environment
+    if (typeof(exports) !== UNDEF_TYPE) {
+        // nodejs env
+        if (typeof module !== UNDEF_TYPE && module.exports) {
+            exports = module.exports = UAParser;
+        }
+        // TODO: test!!!!!!!!
+        /*
+        if (require && require.main === module && process) {
+            // cli
+            var jsonize = function (arr) {
+                var res = [];
+                for (var i in arr) {
+                    res.push(new UAParser(arr[i]).getResult());
+                }
+                process.stdout.write(JSON.stringify(res, null, 2) + '\n');
+            };
+            if (process.stdin.isTTY) {
+                // via args
+                jsonize(process.argv.slice(2));
+            } else {
+                // via pipe
+                var str = '';
+                process.stdin.on('readable', function() {
+                    var read = process.stdin.read();
+                    if (read !== null) {
+                        str += read;
+                    }
+                });
+                process.stdin.on('end', function () {
+                    jsonize(str.replace(/\n$/, '').split('\n'));
+                });
+            }
+        }
+        */
+        exports.UAParser = UAParser;
+    } else {
+        // requirejs env (optional)
+        if (typeof(define) === FUNC_TYPE && define.amd) {
+            define(function () {
+                return UAParser;
+            });
+        } else if (window) {
+            // browser env
+            window.UAParser = UAParser;
+        }
+    }
+
+    // jQuery/Zepto specific (optional)
+    // Note:
+    //   In AMD env the global scope should be kept clean, but jQuery is an exception.
+    //   jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
+    //   and we should catch that.
+    var $ = window && (window.jQuery || window.Zepto);
+    if (typeof $ !== UNDEF_TYPE) {
+        var parser = new UAParser();
+        $.ua = parser.getResult();
+        $.ua.get = function () {
+            return parser.getUA();
+        };
+        $.ua.set = function (uastring) {
+            parser.setUA(uastring);
+            var result = parser.getResult();
+            for (var prop in result) {
+                $.ua[prop] = result[prop];
+            }
+        };
+    }
+
+})(typeof window === 'object' ? window : this);
+
+},{}],22:[function(require,module,exports){
+(function (global){
+
+var rng;
+
+var crypto = global.crypto || global.msCrypto; // for IE 11
+if (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;
+  };
+}
+
+module.exports = rng;
+
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],23:[function(require,module,exports){
+//     uuid.js
+//
+//     Copyright (c) 2010-2012 Robert Kieffer
+//     MIT License - http://opensource.org/licenses/mit-license.php
+
+// 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 = require('./rng');
+
+// 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 === undefined) {
+    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 === undefined) {
+    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 Array(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;
+
+module.exports = uuid;
+
+},{"./rng":22}],24:[function(require,module,exports){
+/*
+WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
+on @visionmedia's Emitter from UI Kit.
+
+Why? I wanted it standalone.
+
+I also wanted support for wildcard emitters like this:
+
+emitter.on('*', function (eventName, other, event, payloads) {
+
+});
+
+emitter.on('somenamespace*', function (eventName, payloads) {
+
+});
+
+Please note that callbacks triggered by wildcard registered events also get
+the event name as the first argument.
+*/
+
+module.exports = WildEmitter;
+
+function WildEmitter() { }
+
+WildEmitter.mixin = function (constructor) {
+    var prototype = constructor.prototype || constructor;
+
+    prototype.isWildEmitter= true;
+
+    // Listen on the given `event` with `fn`. Store a group name if present.
+    prototype.on = function (event, groupName, fn) {
+        this.callbacks = this.callbacks || {};
+        var hasGroup = (arguments.length === 3),
+            group = hasGroup ? arguments[1] : undefined,
+            func = hasGroup ? arguments[2] : arguments[1];
+        func._groupName = group;
+        (this.callbacks[event] = this.callbacks[event] || []).push(func);
+        return this;
+    };
+
+    // Adds an `event` listener that will be invoked a single
+    // time then automatically removed.
+    prototype.once = function (event, groupName, fn) {
+        var self = this,
+            hasGroup = (arguments.length === 3),
+            group = hasGroup ? arguments[1] : undefined,
+            func = hasGroup ? arguments[2] : arguments[1];
+        function on() {
+            self.off(event, on);
+            func.apply(this, arguments);
+        }
+        this.on(event, group, on);
+        return this;
+    };
+
+    // Unbinds an entire group
+    prototype.releaseGroup = function (groupName) {
+        this.callbacks = this.callbacks || {};
+        var item, i, len, handlers;
+        for (item in this.callbacks) {
+            handlers = this.callbacks[item];
+            for (i = 0, len = handlers.length; i < len; i++) {
+                if (handlers[i]._groupName === groupName) {
+                    //console.log('removing');
+                    // remove it and shorten the array we're looping through
+                    handlers.splice(i, 1);
+                    i--;
+                    len--;
+                }
+            }
+        }
+        return this;
+    };
+
+    // Remove the given callback for `event` or all
+    // registered callbacks.
+    prototype.off = function (event, fn) {
+        this.callbacks = this.callbacks || {};
+        var callbacks = this.callbacks[event],
+            i;
+
+        if (!callbacks) return this;
+
+        // remove all handlers
+        if (arguments.length === 1) {
+            delete this.callbacks[event];
+            return this;
+        }
+
+        // remove specific handler
+        i = callbacks.indexOf(fn);
+        callbacks.splice(i, 1);
+        if (callbacks.length === 0) {
+            delete this.callbacks[event];
+        }
+        return this;
+    };
+
+    /// Emit `event` with the given args.
+    // also calls any `*` handlers
+    prototype.emit = function (event) {
+        this.callbacks = this.callbacks || {};
+        var args = [].slice.call(arguments, 1),
+            callbacks = this.callbacks[event],
+            specialCallbacks = this.getWildcardCallbacks(event),
+            i,
+            len,
+            item,
+            listeners;
+
+        if (callbacks) {
+            listeners = callbacks.slice();
+            for (i = 0, len = listeners.length; i < len; ++i) {
+                if (!listeners[i]) {
+                    break;
+                }
+                listeners[i].apply(this, args);
+            }
+        }
+
+        if (specialCallbacks) {
+            len = specialCallbacks.length;
+            listeners = specialCallbacks.slice();
+            for (i = 0, len = listeners.length; i < len; ++i) {
+                if (!listeners[i]) {
+                    break;
+                }
+                listeners[i].apply(this, [event].concat(args));
+            }
+        }
+
+        return this;
+    };
+
+    // Helper for for finding special wildcard event handlers that match the event
+    prototype.getWildcardCallbacks = function (eventName) {
+        this.callbacks = this.callbacks || {};
+        var item,
+            split,
+            result = [];
+
+        for (item in this.callbacks) {
+            split = item.split('*');
+            if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
+                result = result.concat(this.callbacks[item]);
+            }
+        }
+        return result;
+    };
+
+};
+
+WildEmitter.mixin(WildEmitter);
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
diff --git a/bigbluebutton-html5/client/compatibility/reconnecting-websocket.min.js b/bigbluebutton-html5/client/compatibility/reconnecting-websocket.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..3015099ac17bb3ec057e545e21a4db524265d74f
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/reconnecting-websocket.min.js
@@ -0,0 +1 @@
+!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a});
diff --git a/bigbluebutton-html5/client/compatibility/sip.js b/bigbluebutton-html5/client/compatibility/sip.js
new file mode 100644
index 0000000000000000000000000000000000000000..e57c90c2bc1294e6a21680358c176674396d143e
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/sip.js
@@ -0,0 +1,11860 @@
+/*
+ * SIP version 0.7.8
+ * Copyright (c) 2014-2017 Junction Networks, Inc <http://www.onsip.com>
+ * Homepage: http://sipjs.com
+ * License: http://sipjs.com/license/
+ *
+ *
+ * ~~~SIP.js contains substantial portions of JsSIP under the following license~~~
+ * Homepage: http://jssip.net
+ * Copyright (c) 2012-2013 José Luis Millán - Versatica <http://www.versatica.com>
+ *
+ * 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.
+ *
+ * ~~~ end JsSIP license ~~~
+ */
+
+
+(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.SIP = 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){
+// 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;
+}
+
+},{}],2:[function(require,module,exports){
+module.exports={
+  "name": "sip.js",
+  "title": "SIP.js",
+  "description": "A simple, intuitive, and powerful JavaScript signaling library",
+  "version": "0.7.8",
+  "main": "src/index.js",
+  "browser": {
+    "./src/environment.js": "./src/environment_browser.js"
+  },
+  "homepage": "http://sipjs.com",
+  "author": "OnSIP <developer@onsip.com> (http://sipjs.com/authors/)",
+  "contributors": [
+    {
+      "url": "https://github.com/onsip/SIP.js/blob/master/THANKS.md"
+    }
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/onsip/SIP.js.git"
+  },
+  "keywords": [
+    "sip",
+    "websocket",
+    "webrtc",
+    "library",
+    "javascript"
+  ],
+  "devDependencies": {
+    "beefy": "^2.1.5",
+    "browserify": "^4.1.8",
+    "grunt": "~0.4.0",
+    "grunt-browserify": "^4.0.1",
+    "grunt-cli": "~0.1.6",
+    "grunt-contrib-copy": "^0.5.0",
+    "grunt-contrib-jasmine": "^1.0.3",
+    "grunt-contrib-jshint": ">0.5.0",
+    "grunt-contrib-uglify": "~0.2.0",
+    "grunt-peg": "~1.3.1",
+    "grunt-trimtrailingspaces": "^0.4.0",
+    "pegjs": "^0.8.0"
+  },
+  "engines": {
+    "node": ">=0.12"
+  },
+  "license": "MIT",
+  "scripts": {
+    "repl": "beefy test/repl.js --open",
+    "build": "grunt build",
+    "prepublish": "cd src/Grammar && mkdir -p dist && pegjs --extra-options-file peg.json src/Grammar.pegjs dist/Grammar.js",
+    "test": "grunt travis --verbose"
+  },
+  "dependencies": {
+    "ws": "^1.0.1"
+  },
+  "optionalDependencies": {
+    "promiscuous": "^0.6.0"
+  }
+}
+
+},{}],3:[function(require,module,exports){
+"use strict";
+module.exports = function (SIP) {
+var ClientContext;
+
+ClientContext = function (ua, method, target, options) {
+  var originalTarget = target;
+
+  // Validate arguments
+  if (target === undefined) {
+    throw new TypeError('Not enough arguments');
+  }
+
+  this.ua = ua;
+  this.logger = ua.getLogger('sip.clientcontext');
+  this.method = method;
+  target = ua.normalizeTarget(target);
+  if (!target) {
+    throw new TypeError('Invalid target: ' + originalTarget);
+  }
+
+  /* Options
+   * - extraHeaders
+   * - params
+   * - contentType
+   * - body
+   */
+  options = Object.create(options || Object.prototype);
+  options.extraHeaders = (options.extraHeaders || []).slice();
+
+  // Build the request
+  this.request = new SIP.OutgoingRequest(this.method,
+                                         target,
+                                         this.ua,
+                                         options.params,
+                                         options.extraHeaders);
+  if (options.body) {
+    this.body = {};
+    this.body.body = options.body;
+    if (options.contentType) {
+      this.body.contentType = options.contentType;
+    }
+    this.request.body = this.body;
+  }
+
+  /* Set other properties from the request */
+  this.localIdentity = this.request.from;
+  this.remoteIdentity = this.request.to;
+
+  this.data = {};
+};
+ClientContext.prototype = Object.create(SIP.EventEmitter.prototype);
+
+ClientContext.prototype.send = function () {
+  (new SIP.RequestSender(this, this.ua)).send();
+  return this;
+};
+
+ClientContext.prototype.cancel = function (options) {
+  options = options || {};
+
+  options.extraHeaders = (options.extraHeaders || []).slice();
+
+  var cancel_reason = SIP.Utils.getCancelReason(options.status_code, options.reason_phrase);
+  this.request.cancel(cancel_reason, options.extraHeaders);
+
+  this.emit('cancel');
+};
+
+ClientContext.prototype.receiveResponse = function (response) {
+  var cause = SIP.Utils.getReasonPhrase(response.status_code);
+
+  switch(true) {
+    case /^1[0-9]{2}$/.test(response.status_code):
+      this.emit('progress', response, cause);
+      break;
+
+    case /^2[0-9]{2}$/.test(response.status_code):
+      if(this.ua.applicants[this]) {
+        delete this.ua.applicants[this];
+      }
+      this.emit('accepted', response, cause);
+      break;
+
+    default:
+      if(this.ua.applicants[this]) {
+        delete this.ua.applicants[this];
+      }
+      this.emit('rejected', response, cause);
+      this.emit('failed', response, cause);
+      break;
+  }
+
+};
+
+ClientContext.prototype.onRequestTimeout = function () {
+  this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT);
+};
+
+ClientContext.prototype.onTransportError = function () {
+  this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR);
+};
+
+SIP.ClientContext = ClientContext;
+};
+
+},{}],4:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP Constants
+ */
+
+/**
+ * SIP Constants.
+ * @augments SIP
+ */
+
+module.exports = function (name, version) {
+return {
+  USER_AGENT: name +'/'+ version,
+
+  // SIP scheme
+  SIP:  'sip',
+  SIPS: 'sips',
+
+  // End and Failure causes
+  causes: {
+    // Generic error causes
+    CONNECTION_ERROR:         'Connection Error',
+    REQUEST_TIMEOUT:          'Request Timeout',
+    SIP_FAILURE_CODE:         'SIP Failure Code',
+    INTERNAL_ERROR:           'Internal Error',
+
+    // SIP error causes
+    BUSY:                     'Busy',
+    REJECTED:                 'Rejected',
+    REDIRECTED:               'Redirected',
+    UNAVAILABLE:              'Unavailable',
+    NOT_FOUND:                'Not Found',
+    ADDRESS_INCOMPLETE:       'Address Incomplete',
+    INCOMPATIBLE_SDP:         'Incompatible SDP',
+    AUTHENTICATION_ERROR:     'Authentication Error',
+    DIALOG_ERROR:             'Dialog Error',
+
+    // Session error causes
+    WEBRTC_NOT_SUPPORTED:     'WebRTC Not Supported',
+    WEBRTC_ERROR:             'WebRTC Error',
+    CANCELED:                 'Canceled',
+    NO_ANSWER:                'No Answer',
+    EXPIRES:                  'Expires',
+    NO_ACK:                   'No ACK',
+    NO_PRACK:                 'No PRACK',
+    USER_DENIED_MEDIA_ACCESS: 'User Denied Media Access',
+    BAD_MEDIA_DESCRIPTION:    'Bad Media Description',
+    RTP_TIMEOUT:              'RTP Timeout'
+  },
+
+  supported: {
+    UNSUPPORTED:        'none',
+    SUPPORTED:          'supported',
+    REQUIRED:           'required'
+  },
+
+  SIP_ERROR_CAUSES: {
+    REDIRECTED: [300,301,302,305,380],
+    BUSY: [486,600],
+    REJECTED: [403,603],
+    NOT_FOUND: [404,604],
+    UNAVAILABLE: [480,410,408,430],
+    ADDRESS_INCOMPLETE: [484],
+    INCOMPATIBLE_SDP: [488,606],
+    AUTHENTICATION_ERROR:[401,407]
+  },
+
+  // SIP Methods
+  ACK:        'ACK',
+  BYE:        'BYE',
+  CANCEL:     'CANCEL',
+  INFO:       'INFO',
+  INVITE:     'INVITE',
+  MESSAGE:    'MESSAGE',
+  NOTIFY:     'NOTIFY',
+  OPTIONS:    'OPTIONS',
+  REGISTER:   'REGISTER',
+  UPDATE:     'UPDATE',
+  SUBSCRIBE:  'SUBSCRIBE',
+  REFER:      'REFER',
+  PRACK:      'PRACK',
+
+  /* SIP Response Reasons
+   * DOC: http://www.iana.org/assignments/sip-parameters
+   * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7
+   */
+  REASON_PHRASE: {
+    100: 'Trying',
+    180: 'Ringing',
+    181: 'Call Is Being Forwarded',
+    182: 'Queued',
+    183: 'Session Progress',
+    199: 'Early Dialog Terminated',  // draft-ietf-sipcore-199
+    200: 'OK',
+    202: 'Accepted',  // RFC 3265
+    204: 'No Notification',  //RFC 5839
+    300: 'Multiple Choices',
+    301: 'Moved Permanently',
+    302: 'Moved Temporarily',
+    305: 'Use Proxy',
+    380: 'Alternative Service',
+    400: 'Bad Request',
+    401: 'Unauthorized',
+    402: 'Payment Required',
+    403: 'Forbidden',
+    404: 'Not Found',
+    405: 'Method Not Allowed',
+    406: 'Not Acceptable',
+    407: 'Proxy Authentication Required',
+    408: 'Request Timeout',
+    410: 'Gone',
+    412: 'Conditional Request Failed',  // RFC 3903
+    413: 'Request Entity Too Large',
+    414: 'Request-URI Too Long',
+    415: 'Unsupported Media Type',
+    416: 'Unsupported URI Scheme',
+    417: 'Unknown Resource-Priority',  // RFC 4412
+    420: 'Bad Extension',
+    421: 'Extension Required',
+    422: 'Session Interval Too Small',  // RFC 4028
+    423: 'Interval Too Brief',
+    428: 'Use Identity Header',  // RFC 4474
+    429: 'Provide Referrer Identity',  // RFC 3892
+    430: 'Flow Failed',  // RFC 5626
+    433: 'Anonymity Disallowed',  // RFC 5079
+    436: 'Bad Identity-Info',  // RFC 4474
+    437: 'Unsupported Certificate',  // RFC 4744
+    438: 'Invalid Identity Header',  // RFC 4744
+    439: 'First Hop Lacks Outbound Support',  // RFC 5626
+    440: 'Max-Breadth Exceeded',  // RFC 5393
+    469: 'Bad Info Package',  // draft-ietf-sipcore-info-events
+    470: 'Consent Needed',  // RFC 5360
+    478: 'Unresolvable Destination',  // Custom code copied from Kamailio.
+    480: 'Temporarily Unavailable',
+    481: 'Call/Transaction Does Not Exist',
+    482: 'Loop Detected',
+    483: 'Too Many Hops',
+    484: 'Address Incomplete',
+    485: 'Ambiguous',
+    486: 'Busy Here',
+    487: 'Request Terminated',
+    488: 'Not Acceptable Here',
+    489: 'Bad Event',  // RFC 3265
+    491: 'Request Pending',
+    493: 'Undecipherable',
+    494: 'Security Agreement Required',  // RFC 3329
+    500: 'Internal Server Error',
+    501: 'Not Implemented',
+    502: 'Bad Gateway',
+    503: 'Service Unavailable',
+    504: 'Server Time-out',
+    505: 'Version Not Supported',
+    513: 'Message Too Large',
+    580: 'Precondition Failure',  // RFC 3312
+    600: 'Busy Everywhere',
+    603: 'Decline',
+    604: 'Does Not Exist Anywhere',
+    606: 'Not Acceptable'
+  },
+
+  /* SIP Option Tags
+   * DOC: http://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml#sip-parameters-4
+   */
+  OPTION_TAGS: {
+    '100rel':                   true,  // RFC 3262
+    199:                        true,  // RFC 6228
+    answermode:                 true,  // RFC 5373
+    'early-session':            true,  // RFC 3959
+    eventlist:                  true,  // RFC 4662
+    explicitsub:                true,  // RFC-ietf-sipcore-refer-explicit-subscription-03
+    'from-change':              true,  // RFC 4916
+    'geolocation-http':         true,  // RFC 6442
+    'geolocation-sip':          true,  // RFC 6442
+    gin:                        true,  // RFC 6140
+    gruu:                       true,  // RFC 5627
+    histinfo:                   true,  // RFC 7044
+    ice:                        true,  // RFC 5768
+    join:                       true,  // RFC 3911
+    'multiple-refer':           true,  // RFC 5368
+    norefersub:                 true,  // RFC 4488
+    nosub:                      true,  // RFC-ietf-sipcore-refer-explicit-subscription-03
+    outbound:                   true,  // RFC 5626
+    path:                       true,  // RFC 3327
+    policy:                     true,  // RFC 6794
+    precondition:               true,  // RFC 3312
+    pref:                       true,  // RFC 3840
+    privacy:                    true,  // RFC 3323
+    'recipient-list-invite':    true,  // RFC 5366
+    'recipient-list-message':   true,  // RFC 5365
+    'recipient-list-subscribe': true,  // RFC 5367
+    replaces:                   true,  // RFC 3891
+    'resource-priority':        true,  // RFC 4412
+    'sdp-anat':                 true,  // RFC 4092
+    'sec-agree':                true,  // RFC 3329
+    tdialog:                    true,  // RFC 4538
+    timer:                      true,  // RFC 4028
+    uui:                        true   // RFC 7433
+  }
+};
+};
+
+},{}],5:[function(require,module,exports){
+"use strict";
+
+/**
+ * @fileoverview In-Dialog Request Sender
+ */
+
+/**
+ * @augments SIP.Dialog
+ * @class Class creating an In-dialog request sender.
+ * @param {SIP.Dialog} dialog
+ * @param {Object} applicant
+ * @param {SIP.OutgoingRequest} request
+ */
+/**
+ * @fileoverview in-Dialog Request Sender
+ */
+
+module.exports = function (SIP) {
+var RequestSender;
+
+RequestSender = function(dialog, applicant, request) {
+
+  this.dialog = dialog;
+  this.applicant = applicant;
+  this.request = request;
+
+  // RFC3261 14.1 Modifying an Existing Session. UAC Behavior.
+  this.reattempt = false;
+  this.reattemptTimer = null;
+};
+
+RequestSender.prototype = {
+  send: function() {
+    var self = this,
+      request_sender = new SIP.RequestSender(this, this.dialog.owner.ua);
+
+      request_sender.send();
+
+    // RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR-
+    if (this.request.method === SIP.C.INVITE && request_sender.clientTransaction.state !== SIP.Transactions.C.STATUS_TERMINATED) {
+      this.dialog.uac_pending_reply = true;
+      request_sender.clientTransaction.on('stateChanged', function stateChanged(){
+        if (this.state === SIP.Transactions.C.STATUS_ACCEPTED ||
+            this.state === SIP.Transactions.C.STATUS_COMPLETED ||
+            this.state === SIP.Transactions.C.STATUS_TERMINATED) {
+
+          this.removeListener('stateChanged', stateChanged);
+          self.dialog.uac_pending_reply = false;
+
+          if (self.dialog.uas_pending_reply === false) {
+            self.dialog.owner.onReadyToReinvite();
+          }
+        }
+      });
+    }
+  },
+
+  onRequestTimeout: function() {
+    this.applicant.onRequestTimeout();
+  },
+
+  onTransportError: function() {
+    this.applicant.onTransportError();
+  },
+
+  receiveResponse: function(response) {
+    var self = this;
+
+    // RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog.
+    if (response.status_code === 408 || response.status_code === 481) {
+      this.applicant.onDialogError(response);
+    } else if (response.method === SIP.C.INVITE && response.status_code === 491) {
+      if (this.reattempt) {
+        this.applicant.receiveResponse(response);
+      } else {
+        this.request.cseq.value = this.dialog.local_seqnum += 1;
+        this.reattemptTimer = SIP.Timers.setTimeout(
+          function() {
+            if (self.applicant.owner.status !== SIP.Session.C.STATUS_TERMINATED) {
+              self.reattempt = true;
+              self.request_sender.send();
+            }
+          },
+          this.getReattemptTimeout()
+        );
+      }
+    } else {
+      this.applicant.receiveResponse(response);
+    }
+  }
+};
+
+return RequestSender;
+};
+
+},{}],6:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP Dialog
+ */
+
+/**
+ * @augments SIP
+ * @class Class creating a SIP dialog.
+ * @param {SIP.RTCSession} owner
+ * @param {SIP.IncomingRequest|SIP.IncomingResponse} message
+ * @param {Enum} type UAC / UAS
+ * @param {Enum} state SIP.Dialog.C.STATUS_EARLY / SIP.Dialog.C.STATUS_CONFIRMED
+ */
+module.exports = function (SIP) {
+
+var RequestSender = require('./Dialog/RequestSender')(SIP);
+
+var Dialog,
+  C = {
+    // Dialog states
+    STATUS_EARLY:       1,
+    STATUS_CONFIRMED:   2
+  };
+
+// RFC 3261 12.1
+Dialog = function(owner, message, type, state) {
+  var contact;
+
+  this.uac_pending_reply = false;
+  this.uas_pending_reply = false;
+
+  if(!message.hasHeader('contact')) {
+    return {
+      error: 'unable to create a Dialog without Contact header field'
+    };
+  }
+
+  if(message instanceof SIP.IncomingResponse) {
+    state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
+  } else {
+    // Create confirmed dialog if state is not defined
+    state = state || C.STATUS_CONFIRMED;
+  }
+
+  contact = message.parseHeader('contact');
+
+  // RFC 3261 12.1.1
+  if(type === 'UAS') {
+    this.id = {
+      call_id: message.call_id,
+      local_tag: message.to_tag,
+      remote_tag: message.from_tag,
+      toString: function() {
+        return this.call_id + this.local_tag + this.remote_tag;
+      }
+    };
+    this.state = state;
+    this.remote_seqnum = message.cseq;
+    this.local_uri = message.parseHeader('to').uri;
+    this.remote_uri = message.parseHeader('from').uri;
+    this.remote_target = contact.uri;
+    this.route_set = message.getHeaders('record-route');
+    this.invite_seqnum = message.cseq;
+    this.local_seqnum = message.cseq;
+  }
+  // RFC 3261 12.1.2
+  else if(type === 'UAC') {
+    this.id = {
+      call_id: message.call_id,
+      local_tag: message.from_tag,
+      remote_tag: message.to_tag,
+      toString: function() {
+        return this.call_id + this.local_tag + this.remote_tag;
+      }
+    };
+    this.state = state;
+    this.invite_seqnum = message.cseq;
+    this.local_seqnum = message.cseq;
+    this.local_uri = message.parseHeader('from').uri;
+    this.pracked = [];
+    this.remote_uri = message.parseHeader('to').uri;
+    this.remote_target = contact.uri;
+    this.route_set = message.getHeaders('record-route').reverse();
+
+    //RENDERBODY
+    if (this.state === C.STATUS_EARLY && (!owner.hasOffer)) {
+      this.mediaHandler = owner.mediaHandlerFactory(owner);
+    }
+  }
+
+  this.logger = owner.ua.getLogger('sip.dialog', this.id.toString());
+  this.owner = owner;
+  owner.ua.dialogs[this.id.toString()] = this;
+  this.logger.log('new ' + type + ' dialog created with status ' + (this.state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED'));
+  owner.emit('dialog', this);
+};
+
+Dialog.prototype = {
+  /**
+   * @param {SIP.IncomingMessage} message
+   * @param {Enum} UAC/UAS
+   */
+  update: function(message, type) {
+    this.state = C.STATUS_CONFIRMED;
+
+    this.logger.log('dialog '+ this.id.toString() +'  changed to CONFIRMED state');
+
+    if(type === 'UAC') {
+      // RFC 3261 13.2.2.4
+      this.route_set = message.getHeaders('record-route').reverse();
+    }
+  },
+
+  terminate: function() {
+    this.logger.log('dialog ' + this.id.toString() + ' deleted');
+    if (this.mediaHandler && this.state !== C.STATUS_CONFIRMED) {
+      this.mediaHandler.peerConnection.close();
+    }
+    delete this.owner.ua.dialogs[this.id.toString()];
+  },
+
+  /**
+  * @param {String} method request method
+  * @param {Object} extraHeaders extra headers
+  * @returns {SIP.OutgoingRequest}
+  */
+
+  // RFC 3261 12.2.1.1
+  createRequest: function(method, extraHeaders, body) {
+    var cseq, request;
+    extraHeaders = (extraHeaders || []).slice();
+
+    if(!this.local_seqnum) { this.local_seqnum = Math.floor(Math.random() * 10000); }
+
+    cseq = (method === SIP.C.CANCEL || method === SIP.C.ACK) ? this.invite_seqnum : this.local_seqnum += 1;
+
+    request = new SIP.OutgoingRequest(
+      method,
+      this.remote_target,
+      this.owner.ua, {
+        'cseq': cseq,
+        'call_id': this.id.call_id,
+        'from_uri': this.local_uri,
+        'from_tag': this.id.local_tag,
+        'to_uri': this.remote_uri,
+        'to_tag': this.id.remote_tag,
+        'route_set': this.route_set
+      }, extraHeaders, body);
+
+    request.dialog = this;
+
+    return request;
+  },
+
+  /**
+  * @param {SIP.IncomingRequest} request
+  * @returns {Boolean}
+  */
+
+  // RFC 3261 12.2.2
+  checkInDialogRequest: function(request) {
+    var self = this;
+
+    if(!this.remote_seqnum) {
+      this.remote_seqnum = request.cseq;
+    } else if(request.cseq < this.remote_seqnum) {
+        //Do not try to reply to an ACK request.
+        if (request.method !== SIP.C.ACK) {
+          request.reply(500);
+        }
+        if (request.cseq === this.invite_seqnum) {
+          return true;
+        }
+        return false;
+    } else if(request.cseq > this.remote_seqnum) {
+      this.remote_seqnum = request.cseq;
+    }
+
+    switch(request.method) {
+      // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-
+      case SIP.C.INVITE:
+        if (this.uac_pending_reply === true) {
+          request.reply(491);
+        } else if (this.uas_pending_reply === true) {
+          var retryAfter = (Math.random() * 10 | 0) + 1;
+          request.reply(500, null, ['Retry-After:' + retryAfter]);
+          return false;
+        } else {
+          this.uas_pending_reply = true;
+          request.server_transaction.on('stateChanged', function stateChanged(){
+            if (this.state === SIP.Transactions.C.STATUS_ACCEPTED ||
+                this.state === SIP.Transactions.C.STATUS_COMPLETED ||
+                this.state === SIP.Transactions.C.STATUS_TERMINATED) {
+
+              this.removeListener('stateChanged', stateChanged);
+              self.uas_pending_reply = false;
+
+              if (self.uac_pending_reply === false) {
+                self.owner.onReadyToReinvite();
+              }
+            }
+          });
+        }
+
+        // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted
+        if(request.hasHeader('contact')) {
+          request.server_transaction.on('stateChanged', function(){
+            if (this.state === SIP.Transactions.C.STATUS_ACCEPTED) {
+              self.remote_target = request.parseHeader('contact').uri;
+            }
+          });
+        }
+        break;
+      case SIP.C.NOTIFY:
+        // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted
+        if(request.hasHeader('contact')) {
+          request.server_transaction.on('stateChanged', function(){
+            if (this.state === SIP.Transactions.C.STATUS_COMPLETED) {
+              self.remote_target = request.parseHeader('contact').uri;
+            }
+          });
+        }
+        break;
+    }
+
+    return true;
+  },
+
+  sendRequest: function(applicant, method, options) {
+    options = options || {};
+
+    var extraHeaders = (options.extraHeaders || []).slice();
+
+    var body = null;
+    if (options.body) {
+      if (options.body.body) {
+        body = options.body;
+      } else {
+        body = {};
+        body.body = options.body;
+        if (options.contentType) {
+          body.contentType = options.contentType;
+        }
+      }
+    }
+
+    var request = this.createRequest(method, extraHeaders, body),
+      request_sender = new RequestSender(this, applicant, request);
+
+    request_sender.send();
+
+    return request;
+  },
+
+  /**
+  * @param {SIP.IncomingRequest} request
+  */
+  receiveRequest: function(request) {
+    //Check in-dialog request
+    if(!this.checkInDialogRequest(request)) {
+      return;
+    }
+
+    this.owner.receiveRequest(request);
+  }
+};
+
+Dialog.C = C;
+SIP.Dialog = Dialog;
+};
+
+},{"./Dialog/RequestSender":5}],7:[function(require,module,exports){
+"use strict";
+
+/**
+ * @fileoverview SIP Digest Authentication
+ */
+
+/**
+ * SIP Digest Authentication.
+ * @augments SIP.
+ * @function Digest Authentication
+ * @param {SIP.UA} ua
+ */
+module.exports = function (Utils) {
+var DigestAuthentication;
+
+DigestAuthentication = function(ua) {
+  this.logger = ua.getLogger('sipjs.digestauthentication');
+  this.username = ua.configuration.authorizationUser;
+  this.password = ua.configuration.password;
+  this.cnonce = null;
+  this.nc = 0;
+  this.ncHex = '00000000';
+  this.response = null;
+};
+
+
+/**
+* Performs Digest authentication given a SIP request and the challenge
+* received in a response to that request.
+* Returns true if credentials were successfully generated, false otherwise.
+*
+* @param {SIP.OutgoingRequest} request
+* @param {Object} challenge
+*/
+DigestAuthentication.prototype.authenticate = function(request, challenge) {
+  // Inspect and validate the challenge.
+
+  this.algorithm = challenge.algorithm;
+  this.realm = challenge.realm;
+  this.nonce = challenge.nonce;
+  this.opaque = challenge.opaque;
+  this.stale = challenge.stale;
+
+  if (this.algorithm) {
+    if (this.algorithm !== 'MD5') {
+      this.logger.warn('challenge with Digest algorithm different than "MD5", authentication aborted');
+      return false;
+    }
+  } else {
+    this.algorithm = 'MD5';
+  }
+
+  if (! this.realm) {
+    this.logger.warn('challenge without Digest realm, authentication aborted');
+    return false;
+  }
+
+  if (! this.nonce) {
+    this.logger.warn('challenge without Digest nonce, authentication aborted');
+    return false;
+  }
+
+  // 'qop' can contain a list of values (Array). Let's choose just one.
+  if (challenge.qop) {
+    if (challenge.qop.indexOf('auth') > -1) {
+      this.qop = 'auth';
+    } else if (challenge.qop.indexOf('auth-int') > -1) {
+      this.qop = 'auth-int';
+    } else {
+      // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
+      this.logger.warn('challenge without Digest qop different than "auth" or "auth-int", authentication aborted');
+      return false;
+    }
+  } else {
+    this.qop = null;
+  }
+
+  // Fill other attributes.
+
+  this.method = request.method;
+  this.uri = request.ruri;
+  this.cnonce = Utils.createRandomToken(12);
+  this.nc += 1;
+  this.updateNcHex();
+
+  // nc-value = 8LHEX. Max value = 'FFFFFFFF'.
+  if (this.nc === 4294967296) {
+    this.nc = 1;
+    this.ncHex = '00000001';
+  }
+
+  // Calculate the Digest "response" value.
+  this.calculateResponse();
+
+  return true;
+};
+
+
+/**
+* Generate Digest 'response' value.
+* @private
+*/
+DigestAuthentication.prototype.calculateResponse = function() {
+  var ha1, ha2;
+
+  // HA1 = MD5(A1) = MD5(username:realm:password)
+  ha1 = Utils.calculateMD5(this.username + ":" + this.realm + ":" + this.password);
+
+  if (this.qop === 'auth') {
+    // HA2 = MD5(A2) = MD5(method:digestURI)
+    ha2 = Utils.calculateMD5(this.method + ":" + this.uri);
+    // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2)
+    this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth:" + ha2);
+
+  } else if (this.qop === 'auth-int') {
+    // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody))
+    ha2 = Utils.calculateMD5(this.method + ":" + this.uri + ":" + Utils.calculateMD5(this.body ? this.body : ""));
+    // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2)
+    this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth-int:" + ha2);
+
+  } else if (this.qop === null) {
+    // HA2 = MD5(A2) = MD5(method:digestURI)
+    ha2 = Utils.calculateMD5(this.method + ":" + this.uri);
+    // response = MD5(HA1:nonce:HA2)
+    this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + ha2);
+  }
+};
+
+
+/**
+* Return the Proxy-Authorization or WWW-Authorization header value.
+*/
+DigestAuthentication.prototype.toString = function() {
+  var auth_params = [];
+
+  if (! this.response) {
+    throw new Error('response field does not exist, cannot generate Authorization header');
+  }
+
+  auth_params.push('algorithm=' + this.algorithm);
+  auth_params.push('username="' + this.username + '"');
+  auth_params.push('realm="' + this.realm + '"');
+  auth_params.push('nonce="' + this.nonce + '"');
+  auth_params.push('uri="' + this.uri + '"');
+  auth_params.push('response="' + this.response + '"');
+  if (this.opaque) {
+    auth_params.push('opaque="' + this.opaque + '"');
+  }
+  if (this.qop) {
+    auth_params.push('qop=' + this.qop);
+    auth_params.push('cnonce="' + this.cnonce + '"');
+    auth_params.push('nc=' + this.ncHex);
+  }
+
+  return 'Digest ' + auth_params.join(', ');
+};
+
+
+/**
+* Generate the 'nc' value as required by Digest in this.ncHex by reading this.nc.
+* @private
+*/
+DigestAuthentication.prototype.updateNcHex = function() {
+  var hex = Number(this.nc).toString(16);
+  this.ncHex = '00000000'.substr(0, 8-hex.length) + hex;
+};
+
+return DigestAuthentication;
+};
+
+},{}],8:[function(require,module,exports){
+"use strict";
+var NodeEventEmitter = require('events').EventEmitter;
+
+module.exports = function (console) {
+
+// Don't use `new SIP.EventEmitter()` for inheriting.
+// Use Object.create(SIP.EventEmitter.prototoype);
+function EventEmitter () {
+  NodeEventEmitter.call(this);
+}
+
+EventEmitter.prototype = Object.create(NodeEventEmitter.prototype, {
+  constructor: {
+    value: EventEmitter,
+    enumerable: false,
+    writable: true,
+    configurable: true
+  }
+});
+
+EventEmitter.prototype.off = function off (eventName, listener) {
+  var warning = '';
+  warning += 'SIP.EventEmitter#off is deprecated and may be removed in future SIP.js versions.\n';
+  warning += 'Please use removeListener or removeAllListeners instead.\n';
+  warning += 'See here for more details:\n';
+  warning += 'http://nodejs.org/api/events.html#events_emitter_removelistener_event_listener';
+  console.warn(warning);
+
+  if (arguments.length < 2) {
+    return this.removeAllListeners.apply(this, arguments);
+  } else {
+    return this.removeListener(eventName, listener);
+  }
+};
+
+return EventEmitter;
+
+};
+
+},{"events":1}],9:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview Exceptions
+ */
+
+/**
+ * SIP Exceptions.
+ * @augments SIP
+ */
+module.exports = {
+  ConfigurationError: (function(){
+    var exception = function(parameter, value) {
+      this.code = 1;
+      this.name = 'CONFIGURATION_ERROR';
+      this.parameter = parameter;
+      this.value = value;
+      this.message = (!this.value)? 'Missing parameter: '+ this.parameter : 'Invalid value '+ JSON.stringify(this.value) +' for parameter "'+ this.parameter +'"';
+    };
+    exception.prototype = new Error();
+    return exception;
+  }()),
+
+  InvalidStateError: (function(){
+    var exception = function(status) {
+      this.code = 2;
+      this.name = 'INVALID_STATE_ERROR';
+      this.status = status;
+      this.message = 'Invalid status: ' + status;
+    };
+    exception.prototype = new Error();
+    return exception;
+  }()),
+
+  NotSupportedError: (function(){
+    var exception = function(message) {
+      this.code = 3;
+      this.name = 'NOT_SUPPORTED_ERROR';
+      this.message = message;
+    };
+    exception.prototype = new Error();
+    return exception;
+  }()),
+
+  GetDescriptionError: (function(){
+    var exception = function(message) {
+      this.code = 4;
+      this.name = 'GET_DESCRIPTION_ERROR';
+      this.message = message;
+    };
+    exception.prototype = new Error();
+    return exception;
+  }())
+};
+
+},{}],10:[function(require,module,exports){
+"use strict";
+var Grammar = require('./Grammar/dist/Grammar');
+
+module.exports = function (SIP) {
+
+return {
+  parse: function parseCustom (input, startRule) {
+    var options = {startRule: startRule, SIP: SIP};
+    try {
+      Grammar.parse(input, options);
+    } catch (e) {
+      options.data = -1;
+    }
+    return options.data;
+  }
+};
+
+};
+
+},{"./Grammar/dist/Grammar":11}],11:[function(require,module,exports){
+module.exports = (function() {
+  /*
+   * Generated by PEG.js 0.8.0.
+   *
+   * http://pegjs.majda.cz/
+   */
+
+  function peg$subclass(child, parent) {
+    function ctor() { this.constructor = child; }
+    ctor.prototype = parent.prototype;
+    child.prototype = new ctor();
+  }
+
+  function SyntaxError(message, expected, found, offset, line, column) {
+    this.message  = message;
+    this.expected = expected;
+    this.found    = found;
+    this.offset   = offset;
+    this.line     = line;
+    this.column   = column;
+
+    this.name     = "SyntaxError";
+  }
+
+  peg$subclass(SyntaxError, Error);
+
+  function parse(input) {
+    var options = arguments.length > 1 ? arguments[1] : {},
+
+        peg$FAILED = {},
+
+        peg$startRuleIndices = { Contact: 118, Name_Addr_Header: 155, Record_Route: 175, Request_Response: 81, SIP_URI: 45, Subscription_State: 185, Supported: 190, Require: 181, Via: 193, absoluteURI: 84, Call_ID: 117, Content_Disposition: 129, Content_Length: 134, Content_Type: 135, CSeq: 145, displayName: 121, Event: 148, From: 150, host: 52, Max_Forwards: 153, Min_SE: 212, Proxy_Authenticate: 156, quoted_string: 40, Refer_To: 177, Replaces: 178, Session_Expires: 209, stun_URI: 216, To: 191, turn_URI: 223, uuid: 226, WWW_Authenticate: 208, challenge: 157 },
+        peg$startRuleIndex   = 118,
+
+        peg$consts = [
+          "\r\n",
+          { type: "literal", value: "\r\n", description: "\"\\r\\n\"" },
+          /^[0-9]/,
+          { type: "class", value: "[0-9]", description: "[0-9]" },
+          /^[a-zA-Z]/,
+          { type: "class", value: "[a-zA-Z]", description: "[a-zA-Z]" },
+          /^[0-9a-fA-F]/,
+          { type: "class", value: "[0-9a-fA-F]", description: "[0-9a-fA-F]" },
+          /^[\0-\xFF]/,
+          { type: "class", value: "[\\0-\\xFF]", description: "[\\0-\\xFF]" },
+          /^["]/,
+          { type: "class", value: "[\"]", description: "[\"]" },
+          " ",
+          { type: "literal", value: " ", description: "\" \"" },
+          "\t",
+          { type: "literal", value: "\t", description: "\"\\t\"" },
+          /^[a-zA-Z0-9]/,
+          { type: "class", value: "[a-zA-Z0-9]", description: "[a-zA-Z0-9]" },
+          ";",
+          { type: "literal", value: ";", description: "\";\"" },
+          "/",
+          { type: "literal", value: "/", description: "\"/\"" },
+          "?",
+          { type: "literal", value: "?", description: "\"?\"" },
+          ":",
+          { type: "literal", value: ":", description: "\":\"" },
+          "@",
+          { type: "literal", value: "@", description: "\"@\"" },
+          "&",
+          { type: "literal", value: "&", description: "\"&\"" },
+          "=",
+          { type: "literal", value: "=", description: "\"=\"" },
+          "+",
+          { type: "literal", value: "+", description: "\"+\"" },
+          "$",
+          { type: "literal", value: "$", description: "\"$\"" },
+          ",",
+          { type: "literal", value: ",", description: "\",\"" },
+          "-",
+          { type: "literal", value: "-", description: "\"-\"" },
+          "_",
+          { type: "literal", value: "_", description: "\"_\"" },
+          ".",
+          { type: "literal", value: ".", description: "\".\"" },
+          "!",
+          { type: "literal", value: "!", description: "\"!\"" },
+          "~",
+          { type: "literal", value: "~", description: "\"~\"" },
+          "*",
+          { type: "literal", value: "*", description: "\"*\"" },
+          "'",
+          { type: "literal", value: "'", description: "\"'\"" },
+          "(",
+          { type: "literal", value: "(", description: "\"(\"" },
+          ")",
+          { type: "literal", value: ")", description: "\")\"" },
+          peg$FAILED,
+          "%",
+          { type: "literal", value: "%", description: "\"%\"" },
+          null,
+          [],
+          function() {return " "; },
+          function() {return ':'; },
+          /^[!-~]/,
+          { type: "class", value: "[!-~]", description: "[!-~]" },
+          /^[\x80-\uFFFF]/,
+          { type: "class", value: "[\\x80-\\uFFFF]", description: "[\\x80-\\uFFFF]" },
+          /^[\x80-\xBF]/,
+          { type: "class", value: "[\\x80-\\xBF]", description: "[\\x80-\\xBF]" },
+          /^[a-f]/,
+          { type: "class", value: "[a-f]", description: "[a-f]" },
+          "`",
+          { type: "literal", value: "`", description: "\"`\"" },
+          "<",
+          { type: "literal", value: "<", description: "\"<\"" },
+          ">",
+          { type: "literal", value: ">", description: "\">\"" },
+          "\\",
+          { type: "literal", value: "\\", description: "\"\\\\\"" },
+          "[",
+          { type: "literal", value: "[", description: "\"[\"" },
+          "]",
+          { type: "literal", value: "]", description: "\"]\"" },
+          "{",
+          { type: "literal", value: "{", description: "\"{\"" },
+          "}",
+          { type: "literal", value: "}", description: "\"}\"" },
+          function() {return "*"; },
+          function() {return "/"; },
+          function() {return "="; },
+          function() {return "("; },
+          function() {return ")"; },
+          function() {return ">"; },
+          function() {return "<"; },
+          function() {return ","; },
+          function() {return ";"; },
+          function() {return ":"; },
+          function() {return "\""; },
+          /^[!-']/,
+          { type: "class", value: "[!-']", description: "[!-']" },
+          /^[*-[]/,
+          { type: "class", value: "[*-[]", description: "[*-[]" },
+          /^[\]-~]/,
+          { type: "class", value: "[\\]-~]", description: "[\\]-~]" },
+          function(contents) {
+                                  return contents; },
+          /^[#-[]/,
+          { type: "class", value: "[#-[]", description: "[#-[]" },
+          /^[\0-\t]/,
+          { type: "class", value: "[\\0-\\t]", description: "[\\0-\\t]" },
+          /^[\x0B-\f]/,
+          { type: "class", value: "[\\x0B-\\f]", description: "[\\x0B-\\f]" },
+          /^[\x0E-]/,
+          { type: "class", value: "[\\x0E-]", description: "[\\x0E-]" },
+          function() {
+                                  options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port);
+                                  delete options.data.scheme;
+                                  delete options.data.user;
+                                  delete options.data.host;
+                                  delete options.data.host_type;
+                                  delete options.data.port;
+                                },
+          function() {
+                                  options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port, options.data.uri_params, options.data.uri_headers);
+                                  delete options.data.scheme;
+                                  delete options.data.user;
+                                  delete options.data.host;
+                                  delete options.data.host_type;
+                                  delete options.data.port;
+                                  delete options.data.uri_params;
+
+                                  if (options.startRule === 'SIP_URI') { options.data = options.data.uri;}
+                                },
+          "sips",
+          { type: "literal", value: "sips", description: "\"sips\"" },
+          "sip",
+          { type: "literal", value: "sip", description: "\"sip\"" },
+          function(uri_scheme) {
+                              options.data.scheme = uri_scheme; },
+          function() {
+                              options.data.user = decodeURIComponent(text().slice(0, -1));},
+          function() {
+                              options.data.password = text(); },
+          function() {
+                              options.data.host = text();
+                              return options.data.host; },
+          function() {
+                            options.data.host_type = 'domain';
+                            return text(); },
+          /^[a-zA-Z0-9_\-]/,
+          { type: "class", value: "[a-zA-Z0-9_\\-]", description: "[a-zA-Z0-9_\\-]" },
+          /^[a-zA-Z0-9\-]/,
+          { type: "class", value: "[a-zA-Z0-9\\-]", description: "[a-zA-Z0-9\\-]" },
+          function() {
+                              options.data.host_type = 'IPv6';
+                              return text(); },
+          "::",
+          { type: "literal", value: "::", description: "\"::\"" },
+          function() {
+                            options.data.host_type = 'IPv6';
+                            return text(); },
+          function() {
+                              options.data.host_type = 'IPv4';
+                              return text(); },
+          "25",
+          { type: "literal", value: "25", description: "\"25\"" },
+          /^[0-5]/,
+          { type: "class", value: "[0-5]", description: "[0-5]" },
+          "2",
+          { type: "literal", value: "2", description: "\"2\"" },
+          /^[0-4]/,
+          { type: "class", value: "[0-4]", description: "[0-4]" },
+          "1",
+          { type: "literal", value: "1", description: "\"1\"" },
+          /^[1-9]/,
+          { type: "class", value: "[1-9]", description: "[1-9]" },
+          function(port) {
+                              port = parseInt(port.join(''));
+                              options.data.port = port;
+                              return port; },
+          "transport=",
+          { type: "literal", value: "transport=", description: "\"transport=\"" },
+          "udp",
+          { type: "literal", value: "udp", description: "\"udp\"" },
+          "tcp",
+          { type: "literal", value: "tcp", description: "\"tcp\"" },
+          "sctp",
+          { type: "literal", value: "sctp", description: "\"sctp\"" },
+          "tls",
+          { type: "literal", value: "tls", description: "\"tls\"" },
+          function(transport) {
+                                if(!options.data.uri_params) options.data.uri_params={};
+                                options.data.uri_params['transport'] = transport.toLowerCase(); },
+          "user=",
+          { type: "literal", value: "user=", description: "\"user=\"" },
+          "phone",
+          { type: "literal", value: "phone", description: "\"phone\"" },
+          "ip",
+          { type: "literal", value: "ip", description: "\"ip\"" },
+          function(user) {
+                                if(!options.data.uri_params) options.data.uri_params={};
+                                options.data.uri_params['user'] = user.toLowerCase(); },
+          "method=",
+          { type: "literal", value: "method=", description: "\"method=\"" },
+          function(method) {
+                                if(!options.data.uri_params) options.data.uri_params={};
+                                options.data.uri_params['method'] = method; },
+          "ttl=",
+          { type: "literal", value: "ttl=", description: "\"ttl=\"" },
+          function(ttl) {
+                                if(!options.data.params) options.data.params={};
+                                options.data.params['ttl'] = ttl; },
+          "maddr=",
+          { type: "literal", value: "maddr=", description: "\"maddr=\"" },
+          function(maddr) {
+                                if(!options.data.uri_params) options.data.uri_params={};
+                                options.data.uri_params['maddr'] = maddr; },
+          "lr",
+          { type: "literal", value: "lr", description: "\"lr\"" },
+          function() {
+                                if(!options.data.uri_params) options.data.uri_params={};
+                                options.data.uri_params['lr'] = undefined; },
+          function(param, value) {
+                                if(!options.data.uri_params) options.data.uri_params = {};
+                                if (value === null){
+                                  value = undefined;
+                                }
+                                else {
+                                  value = value[1];
+                                }
+                                options.data.uri_params[param.toLowerCase()] = value && value.toLowerCase();},
+          function(hname, hvalue) {
+                                hname = hname.join('').toLowerCase();
+                                hvalue = hvalue.join('');
+                                if(!options.data.uri_headers) options.data.uri_headers = {};
+                                if (!options.data.uri_headers[hname]) {
+                                  options.data.uri_headers[hname] = [hvalue];
+                                } else {
+                                  options.data.uri_headers[hname].push(hvalue);
+                                }},
+          function() {
+                                // lots of tests fail if this isn't guarded...
+                                if (options.startRule === 'Refer_To') {
+                                  options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port, options.data.uri_params, options.data.uri_headers);
+                                  delete options.data.scheme;
+                                  delete options.data.user;
+                                  delete options.data.host;
+                                  delete options.data.host_type;
+                                  delete options.data.port;
+                                  delete options.data.uri_params;
+                                }
+                              },
+          "//",
+          { type: "literal", value: "//", description: "\"//\"" },
+          function() {
+                              options.data.scheme= text(); },
+          { type: "literal", value: "SIP", description: "\"SIP\"" },
+          function() {
+                              options.data.sip_version = text(); },
+          "INVITE",
+          { type: "literal", value: "INVITE", description: "\"INVITE\"" },
+          "ACK",
+          { type: "literal", value: "ACK", description: "\"ACK\"" },
+          "VXACH",
+          { type: "literal", value: "VXACH", description: "\"VXACH\"" },
+          "OPTIONS",
+          { type: "literal", value: "OPTIONS", description: "\"OPTIONS\"" },
+          "BYE",
+          { type: "literal", value: "BYE", description: "\"BYE\"" },
+          "CANCEL",
+          { type: "literal", value: "CANCEL", description: "\"CANCEL\"" },
+          "REGISTER",
+          { type: "literal", value: "REGISTER", description: "\"REGISTER\"" },
+          "SUBSCRIBE",
+          { type: "literal", value: "SUBSCRIBE", description: "\"SUBSCRIBE\"" },
+          "NOTIFY",
+          { type: "literal", value: "NOTIFY", description: "\"NOTIFY\"" },
+          "REFER",
+          { type: "literal", value: "REFER", description: "\"REFER\"" },
+          function() {
+
+                              options.data.method = text();
+                              return options.data.method; },
+          function(status_code) {
+                            options.data.status_code = parseInt(status_code.join('')); },
+          function() {
+                            options.data.reason_phrase = text(); },
+          function() {
+                        options.data = text(); },
+          function() {
+                                  var idx, length;
+                                  length = options.data.multi_header.length;
+                                  for (idx = 0; idx < length; idx++) {
+                                    if (options.data.multi_header[idx].parsed === null) {
+                                      options.data = null;
+                                      break;
+                                    }
+                                  }
+                                  if (options.data !== null) {
+                                    options.data = options.data.multi_header;
+                                  } else {
+                                    options.data = -1;
+                                  }},
+          function() {
+                                  var header;
+                                  if(!options.data.multi_header) options.data.multi_header = [];
+                                  try {
+                                    header = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                                    delete options.data.uri;
+                                    delete options.data.displayName;
+                                    delete options.data.params;
+                                  } catch(e) {
+                                    header = null;
+                                  }
+                                  options.data.multi_header.push( { 'position': peg$currPos,
+                                                            'offset': offset(),
+                                                            'parsed': header
+                                                          });},
+          function(displayName) {
+                                  displayName = text().trim();
+                                  if (displayName[0] === '\"') {
+                                    displayName = displayName.substring(1, displayName.length-1);
+                                  }
+                                  options.data.displayName = displayName; },
+          "q",
+          { type: "literal", value: "q", description: "\"q\"" },
+          function(q) {
+                                  if(!options.data.params) options.data.params = {};
+                                  options.data.params['q'] = q; },
+          "expires",
+          { type: "literal", value: "expires", description: "\"expires\"" },
+          function(expires) {
+                                  if(!options.data.params) options.data.params = {};
+                                  options.data.params['expires'] = expires; },
+          function(delta_seconds) {
+                                  return parseInt(delta_seconds.join('')); },
+          "0",
+          { type: "literal", value: "0", description: "\"0\"" },
+          function() {
+                                  return parseFloat(text()); },
+          function(param, value) {
+                                  if(!options.data.params) options.data.params = {};
+                                  if (value === null){
+                                    value = undefined;
+                                  }
+                                  else {
+                                    value = value[1];
+                                  }
+                                  options.data.params[param.toLowerCase()] = value;},
+          "render",
+          { type: "literal", value: "render", description: "\"render\"" },
+          "session",
+          { type: "literal", value: "session", description: "\"session\"" },
+          "icon",
+          { type: "literal", value: "icon", description: "\"icon\"" },
+          "alert",
+          { type: "literal", value: "alert", description: "\"alert\"" },
+          function() {
+                                      if (options.startRule === 'Content_Disposition') {
+                                        options.data.type = text().toLowerCase();
+                                      }
+                                    },
+          "handling",
+          { type: "literal", value: "handling", description: "\"handling\"" },
+          "optional",
+          { type: "literal", value: "optional", description: "\"optional\"" },
+          "required",
+          { type: "literal", value: "required", description: "\"required\"" },
+          function(length) {
+                                  options.data = parseInt(length.join('')); },
+          function() {
+                                  options.data = text(); },
+          "text",
+          { type: "literal", value: "text", description: "\"text\"" },
+          "image",
+          { type: "literal", value: "image", description: "\"image\"" },
+          "audio",
+          { type: "literal", value: "audio", description: "\"audio\"" },
+          "video",
+          { type: "literal", value: "video", description: "\"video\"" },
+          "application",
+          { type: "literal", value: "application", description: "\"application\"" },
+          "message",
+          { type: "literal", value: "message", description: "\"message\"" },
+          "multipart",
+          { type: "literal", value: "multipart", description: "\"multipart\"" },
+          "x-",
+          { type: "literal", value: "x-", description: "\"x-\"" },
+          function(cseq_value) {
+                            options.data.value=parseInt(cseq_value.join('')); },
+          function(expires) {options.data = expires; },
+          function(event_type) {
+                                 options.data.event = event_type.toLowerCase(); },
+          function() {
+                          var tag = options.data.tag;
+                            options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                            if (tag) {options.data.setParam('tag',tag)}
+                          },
+          "tag",
+          { type: "literal", value: "tag", description: "\"tag\"" },
+          function(tag) {options.data.tag = tag; },
+          function(forwards) {
+                            options.data = parseInt(forwards.join('')); },
+          function(min_expires) {options.data = min_expires; },
+          function() {
+                                  options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                                },
+          "digest",
+          { type: "literal", value: "Digest", description: "\"Digest\"" },
+          "realm",
+          { type: "literal", value: "realm", description: "\"realm\"" },
+          function(realm) { options.data.realm = realm; },
+          "domain",
+          { type: "literal", value: "domain", description: "\"domain\"" },
+          "nonce",
+          { type: "literal", value: "nonce", description: "\"nonce\"" },
+          function(nonce) { options.data.nonce=nonce; },
+          "opaque",
+          { type: "literal", value: "opaque", description: "\"opaque\"" },
+          function(opaque) { options.data.opaque=opaque; },
+          "stale",
+          { type: "literal", value: "stale", description: "\"stale\"" },
+          "true",
+          { type: "literal", value: "true", description: "\"true\"" },
+          function() { options.data.stale=true; },
+          "false",
+          { type: "literal", value: "false", description: "\"false\"" },
+          function() { options.data.stale=false; },
+          "algorithm",
+          { type: "literal", value: "algorithm", description: "\"algorithm\"" },
+          "md5",
+          { type: "literal", value: "MD5", description: "\"MD5\"" },
+          "md5-sess",
+          { type: "literal", value: "MD5-sess", description: "\"MD5-sess\"" },
+          function(algorithm) {
+                                options.data.algorithm=algorithm.toUpperCase(); },
+          "qop",
+          { type: "literal", value: "qop", description: "\"qop\"" },
+          "auth-int",
+          { type: "literal", value: "auth-int", description: "\"auth-int\"" },
+          "auth",
+          { type: "literal", value: "auth", description: "\"auth\"" },
+          function(qop_value) {
+                                  options.data.qop || (options.data.qop=[]);
+                                  options.data.qop.push(qop_value.toLowerCase()); },
+          function(rack_value) {
+                            options.data.value=parseInt(rack_value.join('')); },
+          function() {
+                            var idx, length;
+                            length = options.data.multi_header.length;
+                            for (idx = 0; idx < length; idx++) {
+                              if (options.data.multi_header[idx].parsed === null) {
+                                options.data = null;
+                                break;
+                              }
+                            }
+                            if (options.data !== null) {
+                              options.data = options.data.multi_header;
+                            } else {
+                              options.data = -1;
+                            }},
+          function() {
+                            var header;
+                            if(!options.data.multi_header) options.data.multi_header = [];
+                            try {
+                              header = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                              delete options.data.uri;
+                              delete options.data.displayName;
+                              delete options.data.params;
+                            } catch(e) {
+                              header = null;
+                            }
+                            options.data.multi_header.push( { 'position': peg$currPos,
+                                                      'offset': offset(),
+                                                      'parsed': header
+                                                    });},
+          function() {
+                        options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                      },
+          function() {
+                                if (!(options.data.replaces_from_tag && options.data.replaces_to_tag)) {
+                                  options.data = -1;
+                                }
+                              },
+          function() {
+                                options.data = {
+                                  call_id: options.data
+                                };
+                              },
+          "from-tag",
+          { type: "literal", value: "from-tag", description: "\"from-tag\"" },
+          function(from_tag) {
+                                options.data.replaces_from_tag = from_tag;
+                              },
+          "to-tag",
+          { type: "literal", value: "to-tag", description: "\"to-tag\"" },
+          function(to_tag) {
+                                options.data.replaces_to_tag = to_tag;
+                              },
+          "early-only",
+          { type: "literal", value: "early-only", description: "\"early-only\"" },
+          function() {
+                                options.data.early_only = true;
+                              },
+          function(r) {return r;},
+          function(first, rest) { return list(first, rest); },
+          function(value) {
+                          if (options.startRule === 'Require') {
+                            options.data = value || [];
+                          }
+                        },
+          function(rseq_value) {
+                            options.data.value=parseInt(rseq_value.join('')); },
+          "active",
+          { type: "literal", value: "active", description: "\"active\"" },
+          "pending",
+          { type: "literal", value: "pending", description: "\"pending\"" },
+          "terminated",
+          { type: "literal", value: "terminated", description: "\"terminated\"" },
+          function() {
+                                  options.data.state = text(); },
+          "reason",
+          { type: "literal", value: "reason", description: "\"reason\"" },
+          function(reason) {
+                                  if (typeof reason !== 'undefined') options.data.reason = reason; },
+          function(expires) {
+                                  if (typeof expires !== 'undefined') options.data.expires = expires; },
+          "retry_after",
+          { type: "literal", value: "retry_after", description: "\"retry_after\"" },
+          function(retry_after) {
+                                  if (typeof retry_after !== 'undefined') options.data.retry_after = retry_after; },
+          "deactivated",
+          { type: "literal", value: "deactivated", description: "\"deactivated\"" },
+          "probation",
+          { type: "literal", value: "probation", description: "\"probation\"" },
+          "rejected",
+          { type: "literal", value: "rejected", description: "\"rejected\"" },
+          "timeout",
+          { type: "literal", value: "timeout", description: "\"timeout\"" },
+          "giveup",
+          { type: "literal", value: "giveup", description: "\"giveup\"" },
+          "noresource",
+          { type: "literal", value: "noresource", description: "\"noresource\"" },
+          "invariant",
+          { type: "literal", value: "invariant", description: "\"invariant\"" },
+          function(value) {
+                          if (options.startRule === 'Supported') {
+                            options.data = value || [];
+                          }
+                        },
+          function() {
+                        var tag = options.data.tag;
+                          options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params);
+                          if (tag) {options.data.setParam('tag',tag)}
+                        },
+          "ttl",
+          { type: "literal", value: "ttl", description: "\"ttl\"" },
+          function(via_ttl_value) {
+                                options.data.ttl = via_ttl_value; },
+          "maddr",
+          { type: "literal", value: "maddr", description: "\"maddr\"" },
+          function(via_maddr) {
+                                options.data.maddr = via_maddr; },
+          "received",
+          { type: "literal", value: "received", description: "\"received\"" },
+          function(via_received) {
+                                options.data.received = via_received; },
+          "branch",
+          { type: "literal", value: "branch", description: "\"branch\"" },
+          function(via_branch) {
+                                options.data.branch = via_branch; },
+          "rport",
+          { type: "literal", value: "rport", description: "\"rport\"" },
+          function() {
+                                if(typeof response_port !== 'undefined')
+                                  options.data.rport = response_port.join(''); },
+          function(via_protocol) {
+                                options.data.protocol = via_protocol; },
+          { type: "literal", value: "UDP", description: "\"UDP\"" },
+          { type: "literal", value: "TCP", description: "\"TCP\"" },
+          { type: "literal", value: "TLS", description: "\"TLS\"" },
+          { type: "literal", value: "SCTP", description: "\"SCTP\"" },
+          function(via_transport) {
+                                options.data.transport = via_transport; },
+          function() {
+                                options.data.host = text(); },
+          function(via_sent_by_port) {
+                                options.data.port = parseInt(via_sent_by_port.join('')); },
+          function(ttl) {
+                                return parseInt(ttl.join('')); },
+          function(deltaSeconds) {
+                                if (options.startRule === 'Session_Expires') {
+                                  options.data.deltaSeconds = deltaSeconds;
+                                }
+                              },
+          "refresher",
+          { type: "literal", value: "refresher", description: "\"refresher\"" },
+          "uas",
+          { type: "literal", value: "uas", description: "\"uas\"" },
+          "uac",
+          { type: "literal", value: "uac", description: "\"uac\"" },
+          function(endpoint) {
+                                if (options.startRule === 'Session_Expires') {
+                                  options.data.refresher = endpoint;
+                                }
+                              },
+          function(deltaSeconds) {
+                                if (options.startRule === 'Min_SE') {
+                                  options.data = deltaSeconds;
+                                }
+                              },
+          "stuns",
+          { type: "literal", value: "stuns", description: "\"stuns\"" },
+          "stun",
+          { type: "literal", value: "stun", description: "\"stun\"" },
+          function(scheme) {
+                                options.data.scheme = scheme; },
+          function(host) {
+                                options.data.host = host; },
+          "?transport=",
+          { type: "literal", value: "?transport=", description: "\"?transport=\"" },
+          "turns",
+          { type: "literal", value: "turns", description: "\"turns\"" },
+          "turn",
+          { type: "literal", value: "turn", description: "\"turn\"" },
+          function() {
+                                options.data.transport = transport; },
+          function() {
+                            options.data = text(); }
+        ],
+
+        peg$bytecode = [
+          peg$decode(". \"\"2 3!"),
+          peg$decode("0\"\"\"1!3#"),
+          peg$decode("0$\"\"1!3%"),
+          peg$decode("0&\"\"1!3'"),
+          peg$decode("7'*# \"7("),
+          peg$decode("0(\"\"1!3)"),
+          peg$decode("0*\"\"1!3+"),
+          peg$decode(".,\"\"2,3-"),
+          peg$decode("..\"\"2.3/"),
+          peg$decode("00\"\"1!31"),
+          peg$decode(".2\"\"2233*\x89 \".4\"\"2435*} \".6\"\"2637*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"),
+          peg$decode("7)*# \"7,"),
+          peg$decode(".F\"\"2F3G*} \".H\"\"2H3I*q \".J\"\"2J3K*e \".L\"\"2L3M*Y \".N\"\"2N3O*M \".P\"\"2P3Q*A \".R\"\"2R3S*5 \".T\"\"2T3U*) \".V\"\"2V3W"),
+          peg$decode("!!.Y\"\"2Y3Z+7$7#+-%7#+#%'#%$## X$\"# X\"# X+! (%"),
+          peg$decode("!! \\7$,#&7$\"+-$7 +#%'\"%$\"# X\"# X*# \" [+@$ \\7$+&$,#&7$\"\"\" X+'%4\"6]\" %$\"# X\"# X"),
+          peg$decode("7.*# \" ["),
+          peg$decode("! \\7'*# \"7(,)&7'*# \"7(\"+A$.8\"\"2839+1%7/+'%4#6^# %$## X$\"# X\"# X"),
+          peg$decode("!! \\72+&$,#&72\"\"\" X+o$ \\! \\7.,#&7.\"+-$72+#%'\"%$\"# X\"# X,@&! \\7.,#&7.\"+-$72+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+! (%"),
+          peg$decode("0_\"\"1!3`*# \"73"),
+          peg$decode("0a\"\"1!3b"),
+          peg$decode("0c\"\"1!3d"),
+          peg$decode("7!*) \"0e\"\"1!3f"),
+          peg$decode("! \\7)*\x95 \".F\"\"2F3G*\x89 \".J\"\"2J3K*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O+\x9E$,\x9B&7)*\x95 \".F\"\"2F3G*\x89 \".J\"\"2J3K*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+! (%"),
+          peg$decode("! \\7)*\x89 \".F\"\"2F3G*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O+\x92$,\x8F&7)*\x89 \".F\"\"2F3G*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+! (%"),
+          peg$decode(".T\"\"2T3U*\xE3 \".V\"\"2V3W*\xD7 \".i\"\"2i3j*\xCB \".k\"\"2k3l*\xBF \".:\"\"2:3;*\xB3 \".D\"\"2D3E*\xA7 \".2\"\"2233*\x9B \".8\"\"2839*\x8F \".m\"\"2m3n*\x83 \"7&*} \".4\"\"2435*q \".o\"\"2o3p*e \".q\"\"2q3r*Y \".6\"\"2637*M \".>\"\"2>3?*A \".s\"\"2s3t*5 \".u\"\"2u3v*) \"7'*# \"7("),
+          peg$decode("! \\7)*\u012B \".F\"\"2F3G*\u011F \".J\"\"2J3K*\u0113 \".L\"\"2L3M*\u0107 \".Y\"\"2Y3Z*\xFB \".P\"\"2P3Q*\xEF \".H\"\"2H3I*\xE3 \".@\"\"2@3A*\xD7 \".g\"\"2g3h*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".i\"\"2i3j*\x8F \".k\"\"2k3l*\x83 \".8\"\"2839*w \".m\"\"2m3n*k \"7&*e \".4\"\"2435*Y \".o\"\"2o3p*M \".q\"\"2q3r*A \".6\"\"2637*5 \".s\"\"2s3t*) \".u\"\"2u3v+\u0134$,\u0131&7)*\u012B \".F\"\"2F3G*\u011F \".J\"\"2J3K*\u0113 \".L\"\"2L3M*\u0107 \".Y\"\"2Y3Z*\xFB \".P\"\"2P3Q*\xEF \".H\"\"2H3I*\xE3 \".@\"\"2@3A*\xD7 \".g\"\"2g3h*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".i\"\"2i3j*\x8F \".k\"\"2k3l*\x83 \".8\"\"2839*w \".m\"\"2m3n*k \"7&*e \".4\"\"2435*Y \".o\"\"2o3p*M \".q\"\"2q3r*A \".6\"\"2637*5 \".s\"\"2s3t*) \".u\"\"2u3v\"\"\" X+! (%"),
+          peg$decode("!7/+A$.P\"\"2P3Q+1%7/+'%4#6w# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.4\"\"2435+1%7/+'%4#6x# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.>\"\"2>3?+1%7/+'%4#6y# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.T\"\"2T3U+1%7/+'%4#6z# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.V\"\"2V3W+1%7/+'%4#6{# %$## X$\"# X\"# X"),
+          peg$decode("!.k\"\"2k3l+1$7/+'%4\"6|\" %$\"# X\"# X"),
+          peg$decode("!7/+7$.i\"\"2i3j+'%4\"6}\" %$\"# X\"# X"),
+          peg$decode("!7/+A$.D\"\"2D3E+1%7/+'%4#6~# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.2\"\"2233+1%7/+'%4#6# %$## X$\"# X\"# X"),
+          peg$decode("!7/+A$.8\"\"2839+1%7/+'%4#6\x80# %$## X$\"# X\"# X"),
+          peg$decode("!7/+1$7&+'%4\"6\x81\" %$\"# X\"# X"),
+          peg$decode("!7&+1$7/+'%4\"6\x81\" %$\"# X\"# X"),
+          peg$decode("!7=+W$ \\7G*) \"7K*# \"7F,/&7G*) \"7K*# \"7F\"+-%7>+#%'#%$## X$\"# X\"# X"),
+          peg$decode("0\x82\"\"1!3\x83*A \"0\x84\"\"1!3\x85*5 \"0\x86\"\"1!3\x87*) \"73*# \"7."),
+          peg$decode("!!7/+U$7&+K% \\7J*# \"7K,)&7J*# \"7K\"+-%7&+#%'$%$$# X$## X$\"# X\"# X+! (%"),
+          peg$decode("!7/+`$7&+V%! \\7J*# \"7K,)&7J*# \"7K\"+! (%+2%7&+(%4$6\x88$!!%$$# X$## X$\"# X\"# X"),
+          peg$decode("7.*G \".L\"\"2L3M*; \"0\x89\"\"1!3\x8A*/ \"0\x86\"\"1!3\x87*# \"73"),
+          peg$decode("!.m\"\"2m3n+K$0\x8B\"\"1!3\x8C*5 \"0\x8D\"\"1!3\x8E*) \"0\x8F\"\"1!3\x90+#%'\"%$\"# X\"# X"),
+          peg$decode("!7N+Q$.8\"\"2839+A%7O*# \" [+1%7S+'%4$6\x91$ %$$# X$## X$\"# X\"# X"),
+          peg$decode("!7N+k$.8\"\"2839+[%7O*# \" [+K%7S+A%7_+7%7l*# \" [+'%4&6\x92& %$&# X$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!/\x93\"\"1$3\x94*) \"/\x95\"\"1#3\x96+' 4!6\x97!! %"),
+          peg$decode("!7P+b$!.8\"\"2839+-$7R+#%'\"%$\"# X\"# X*# \" [+7%.:\"\"2:3;+'%4#6\x98# %$## X$\"# X\"# X"),
+          peg$decode(" \\7+*) \"7-*# \"7Q+2$,/&7+*) \"7-*# \"7Q\"\"\" X"),
+          peg$decode(".<\"\"2<3=*q \".>\"\"2>3?*e \".@\"\"2@3A*Y \".B\"\"2B3C*M \".D\"\"2D3E*A \".2\"\"2233*5 \".6\"\"2637*) \".4\"\"2435"),
+          peg$decode("! \\7+*_ \"7-*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E,e&7+*_ \"7-*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E\"+& 4!6\x99! %"),
+          peg$decode("!7T+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"),
+          peg$decode("!7U*) \"7\\*# \"7X+& 4!6\x9A! %"),
+          peg$decode("! \\!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X,>&!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X\"+G$7W+=%.J\"\"2J3K*# \" [+'%4#6\x9B# %$## X$\"# X\"# X"),
+          peg$decode(" \\0\x9C\"\"1!3\x9D+,$,)&0\x9C\"\"1!3\x9D\"\"\" X"),
+          peg$decode("!0$\"\"1!3%+A$ \\0\x9E\"\"1!3\x9F,)&0\x9E\"\"1!3\x9F\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!.o\"\"2o3p+A$7Y+7%.q\"\"2q3r+'%4#6\xA0# %$## X$\"# X\"# X"),
+          peg$decode("!!7Z+\xBF$.8\"\"2839+\xAF%7Z+\xA5%.8\"\"2839+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'-%$-# X$,# X$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0838 \"!.\xA1\"\"2\xA13\xA2+\xAF$7Z+\xA5%.8\"\"2839+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%',%$,# X$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0795 \"!.\xA1\"\"2\xA13\xA2+\x95$7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'*%$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u070C \"!.\xA1\"\"2\xA13\xA2+{$7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u069D \"!.\xA1\"\"2\xA13\xA2+a$7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'&%$&# X$%# X$$# X$## X$\"# X\"# X*\u0648 \"!.\xA1\"\"2\xA13\xA2+G$7Z+=%.8\"\"2839+-%7[+#%'$%$$# X$## X$\"# X\"# X*\u060D \"!.\xA1\"\"2\xA13\xA2+-$7[+#%'\"%$\"# X\"# X*\u05EC \"!.\xA1\"\"2\xA13\xA2+-$7Z+#%'\"%$\"# X\"# X*\u05CB \"!7Z+\xA5$.\xA1\"\"2\xA13\xA2+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'+%$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0538 \"!7Z+\xB6$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x8B%.\xA1\"\"2\xA13\xA2+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'*%$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0494 \"!7Z+\xC7$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x9C%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+q%.\xA1\"\"2\xA13\xA2+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%')%$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u03DF \"!7Z+\xD8$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xAD%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x82%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+W%.\xA1\"\"2\xA13\xA2+G%7Z+=%.8\"\"2839+-%7[+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0319 \"!7Z+\xE9$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xBE%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x93%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+h%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+=%.\xA1\"\"2\xA13\xA2+-%7[+#%''%$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0242 \"!7Z+\u0114$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xE9%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xBE%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x93%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+h%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+=%.\xA1\"\"2\xA13\xA2+-%7Z+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0140 \"!7Z+\u0135$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\u010A%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xDF%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xB4%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x89%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+^%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+3%.\xA1\"\"2\xA13\xA2+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X+& 4!6\xA3! %"),
+          peg$decode("!7#+S$7#*# \" [+C%7#*# \" [+3%7#*# \" [+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("!7Z+=$.8\"\"2839+-%7Z+#%'#%$## X$\"# X\"# X*# \"7\\"),
+          peg$decode("!7]+u$.J\"\"2J3K+e%7]+[%.J\"\"2J3K+K%7]+A%.J\"\"2J3K+1%7]+'%4'6\xA4' %$'# X$&# X$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!.\xA5\"\"2\xA53\xA6+3$0\xA7\"\"1!3\xA8+#%'\"%$\"# X\"# X*\xA0 \"!.\xA9\"\"2\xA93\xAA+=$0\xAB\"\"1!3\xAC+-%7!+#%'#%$## X$\"# X\"# X*o \"!.\xAD\"\"2\xAD3\xAE+7$7!+-%7!+#%'#%$## X$\"# X\"# X*D \"!0\xAF\"\"1!3\xB0+-$7!+#%'\"%$\"# X\"# X*# \"7!"),
+          peg$decode("!!7!*# \" [+c$7!*# \" [+S%7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\xB1!! %"),
+          peg$decode(" \\!.2\"\"2233+-$7`+#%'\"%$\"# X\"# X,>&!.2\"\"2233+-$7`+#%'\"%$\"# X\"# X\""),
+          peg$decode("7a*A \"7b*; \"7c*5 \"7d*/ \"7e*) \"7f*# \"7g"),
+          peg$decode("!/\xB2\"\"1*3\xB3+b$/\xB4\"\"1#3\xB5*G \"/\xB6\"\"1#3\xB7*; \"/\xB8\"\"1$3\xB9*/ \"/\xBA\"\"1#3\xBB*# \"76+(%4\"6\xBC\"! %$\"# X\"# X"),
+          peg$decode("!/\xBD\"\"1%3\xBE+J$/\xBF\"\"1%3\xC0*/ \"/\xC1\"\"1\"3\xC2*# \"76+(%4\"6\xC3\"! %$\"# X\"# X"),
+          peg$decode("!/\xC4\"\"1'3\xC5+2$7\x8F+(%4\"6\xC6\"! %$\"# X\"# X"),
+          peg$decode("!/\xC7\"\"1$3\xC8+2$7\xEF+(%4\"6\xC9\"! %$\"# X\"# X"),
+          peg$decode("!/\xCA\"\"1&3\xCB+2$7T+(%4\"6\xCC\"! %$\"# X\"# X"),
+          peg$decode("!/\xCD\"\"1\"3\xCE+R$!.>\"\"2>3?+-$76+#%'\"%$\"# X\"# X*# \" [+'%4\"6\xCF\" %$\"# X\"# X"),
+          peg$decode("!7h+T$!.>\"\"2>3?+-$7i+#%'\"%$\"# X\"# X*# \" [+)%4\"6\xD0\"\"! %$\"# X\"# X"),
+          peg$decode("! \\7j+&$,#&7j\"\"\" X+! (%"),
+          peg$decode("! \\7j+&$,#&7j\"\"\" X+! (%"),
+          peg$decode("7k*) \"7+*# \"7-"),
+          peg$decode(".o\"\"2o3p*e \".q\"\"2q3r*Y \".4\"\"2435*M \".8\"\"2839*A \".<\"\"2<3=*5 \".@\"\"2@3A*) \".B\"\"2B3C"),
+          peg$decode("!.6\"\"2637+u$7m+k% \\!.<\"\"2<3=+-$7m+#%'\"%$\"# X\"# X,>&!.<\"\"2<3=+-$7m+#%'\"%$\"# X\"# X\"+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!7n+C$.>\"\"2>3?+3%7o+)%4#6\xD1#\"\" %$## X$\"# X\"# X"),
+          peg$decode(" \\7p*) \"7+*# \"7-+2$,/&7p*) \"7+*# \"7-\"\"\" X"),
+          peg$decode(" \\7p*) \"7+*# \"7-,/&7p*) \"7+*# \"7-\""),
+          peg$decode(".o\"\"2o3p*e \".q\"\"2q3r*Y \".4\"\"2435*M \".6\"\"2637*A \".8\"\"2839*5 \".@\"\"2@3A*) \".B\"\"2B3C"),
+          peg$decode("7\x90*# \"7r"),
+          peg$decode("!7\x8F+K$7'+A%7s+7%7'+-%7\x84+#%'%%$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("7M*# \"7t"),
+          peg$decode("!7+G$.8\"\"2839+7%7u*# \"7x+'%4#6\xD2# %$## X$\"# X\"# X"),
+          peg$decode("!7v*# \"7w+N$!.6\"\"2637+-$7\x83+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"),
+          peg$decode("!.\xD3\"\"2\xD33\xD4+=$7\x80+3%7w*# \" [+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!.4\"\"2435+-$7{+#%'\"%$\"# X\"# X"),
+          peg$decode("!7z+5$ \\7y,#&7y\"+#%'\"%$\"# X\"# X"),
+          peg$decode("7**) \"7+*# \"7-"),
+          peg$decode("7+*\x8F \"7-*\x89 \".2\"\"2233*} \".6\"\"2637*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"),
+          peg$decode("!7|+k$ \\!.4\"\"2435+-$7|+#%'\"%$\"# X\"# X,>&!.4\"\"2435+-$7|+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("! \\7~,#&7~\"+k$ \\!.2\"\"2233+-$7}+#%'\"%$\"# X\"# X,>&!.2\"\"2233+-$7}+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode(" \\7~,#&7~\""),
+          peg$decode("7+*w \"7-*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"),
+          peg$decode("!7\"+\x8D$ \\7\"*G \"7!*A \".@\"\"2@3A*5 \".F\"\"2F3G*) \".J\"\"2J3K,M&7\"*G \"7!*A \".@\"\"2@3A*5 \".F\"\"2F3G*) \".J\"\"2J3K\"+'%4\"6\xD5\" %$\"# X\"# X"),
+          peg$decode("7\x81*# \"7\x82"),
+          peg$decode("!!7O+3$.:\"\"2:3;+#%'\"%$\"# X\"# X*# \" [+-$7S+#%'\"%$\"# X\"# X*# \" ["),
+          peg$decode(" \\7+*\x83 \"7-*} \".B\"\"2B3C*q \".D\"\"2D3E*e \".2\"\"2233*Y \".8\"\"2839*M \".:\"\"2:3;*A \".<\"\"2<3=*5 \".>\"\"2>3?*) \".@\"\"2@3A+\x8C$,\x89&7+*\x83 \"7-*} \".B\"\"2B3C*q \".D\"\"2D3E*e \".2\"\"2233*Y \".8\"\"2839*M \".:\"\"2:3;*A \".<\"\"2<3=*5 \".>\"\"2>3?*) \".@\"\"2@3A\"\"\" X"),
+          peg$decode(" \\7y,#&7y\""),
+          peg$decode("!/\x95\"\"1#3\xD6+y$.4\"\"2435+i% \\7!+&$,#&7!\"\"\" X+P%.J\"\"2J3K+@% \\7!+&$,#&7!\"\"\" X+'%4%6\xD7% %$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode(".\xD8\"\"2\xD83\xD9"),
+          peg$decode(".\xDA\"\"2\xDA3\xDB"),
+          peg$decode(".\xDC\"\"2\xDC3\xDD"),
+          peg$decode(".\xDE\"\"2\xDE3\xDF"),
+          peg$decode(".\xE0\"\"2\xE03\xE1"),
+          peg$decode(".\xE2\"\"2\xE23\xE3"),
+          peg$decode(".\xE4\"\"2\xE43\xE5"),
+          peg$decode(".\xE6\"\"2\xE63\xE7"),
+          peg$decode(".\xE8\"\"2\xE83\xE9"),
+          peg$decode(".\xEA\"\"2\xEA3\xEB"),
+          peg$decode("!7\x85*S \"7\x86*M \"7\x88*G \"7\x89*A \"7\x8A*; \"7\x8B*5 \"7\x8C*/ \"7\x8D*) \"7\x8E*# \"76+& 4!6\xEC! %"),
+          peg$decode("!7\x84+K$7'+A%7\x91+7%7'+-%7\x93+#%'%%$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!7\x92+' 4!6\xED!! %"),
+          peg$decode("!7!+7$7!+-%7!+#%'#%$## X$\"# X\"# X"),
+          peg$decode("! \\7**A \"7+*; \"7-*5 \"73*/ \"74*) \"7'*# \"7(,G&7**A \"7+*; \"7-*5 \"73*/ \"74*) \"7'*# \"7(\"+& 4!6\xEE! %"),
+          peg$decode("!7\xB5+_$ \\!7A+-$7\xB5+#%'\"%$\"# X\"# X,8&!7A+-$7\xB5+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!79+R$!.:\"\"2:3;+-$79+#%'\"%$\"# X\"# X*# \" [+'%4\"6\xEF\" %$\"# X\"# X"),
+          peg$decode("!7:*j \"!7\x97+_$ \\!7A+-$7\x97+#%'\"%$\"# X\"# X,8&!7A+-$7\x97+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+& 4!6\xF0! %"),
+          peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\x9A+#%'\"%$\"# X\"# X,8&!7B+-$7\x9A+#%'\"%$\"# X\"# X\"+'%4\"6\xF1\" %$\"# X\"# X"),
+          peg$decode("!7\x99*# \" [+A$7@+7%7M+-%7?+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("!!76+_$ \\!7.+-$76+#%'\"%$\"# X\"# X,8&!7.+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X*# \"7H+' 4!6\xF2!! %"),
+          peg$decode("7\x9B*) \"7\x9C*# \"7\x9F"),
+          peg$decode("!/\xF3\"\"1!3\xF4+<$7<+2%7\x9E+(%4#6\xF5#! %$## X$\"# X\"# X"),
+          peg$decode("!/\xF6\"\"1'3\xF7+<$7<+2%7\x9D+(%4#6\xF8#! %$## X$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\xF9!! %"),
+          peg$decode("!.\xFA\"\"2\xFA3\xFB+x$!.J\"\"2J3K+S$7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'$%$$# X$## X$\"# X\"# X*# \" [+'%4\"6\xFC\" %$\"# X\"# X"),
+          peg$decode("!76+N$!7<+-$7\xA0+#%'\"%$\"# X\"# X*# \" [+)%4\"6\xFD\"\"! %$\"# X\"# X"),
+          peg$decode("76*) \"7T*# \"7H"),
+          peg$decode("!7\xA2+_$ \\!7B+-$7\xA3+#%'\"%$\"# X\"# X,8&!7B+-$7\xA3+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!/\xFE\"\"1&3\xFF*G \"/\u0100\"\"1'3\u0101*; \"/\u0102\"\"1$3\u0103*/ \"/\u0104\"\"1%3\u0105*# \"76+& 4!6\u0106! %"),
+          peg$decode("7\xA4*# \"7\x9F"),
+          peg$decode("!/\u0107\"\"1(3\u0108+O$7<+E%/\u0109\"\"1(3\u010A*/ \"/\u010B\"\"1(3\u010C*# \"76+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!76+_$ \\!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u010D!! %"),
+          peg$decode("!7\xA8+& 4!6\u010E! %"),
+          peg$decode("!7\xA9+s$7;+i%7\xAE+_% \\!7B+-$7\xAF+#%'\"%$\"# X\"# X,8&!7B+-$7\xAF+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("7\xAA*# \"7\xAB"),
+          peg$decode("/\u010F\"\"1$3\u0110*S \"/\u0111\"\"1%3\u0112*G \"/\u0113\"\"1%3\u0114*; \"/\u0115\"\"1%3\u0116*/ \"/\u0117\"\"1+3\u0118*# \"7\xAC"),
+          peg$decode("/\u0119\"\"1'3\u011A*/ \"/\u011B\"\"1)3\u011C*# \"7\xAC"),
+          peg$decode("76*# \"7\xAD"),
+          peg$decode("!/\u011D\"\"1\"3\u011E+-$76+#%'\"%$\"# X\"# X"),
+          peg$decode("7\xAC*# \"76"),
+          peg$decode("!76+7$7<+-%7\xB0+#%'#%$## X$\"# X\"# X"),
+          peg$decode("76*# \"7H"),
+          peg$decode("!7\xB2+7$7.+-%7\x8F+#%'#%$## X$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u011F!! %"),
+          peg$decode("!7\x9D+' 4!6\u0120!! %"),
+          peg$decode("!7\xB5+d$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+(%4\"6\u0121\"!!%$\"# X\"# X"),
+          peg$decode("!!77+k$ \\!.J\"\"2J3K+-$77+#%'\"%$\"# X\"# X,>&!.J\"\"2J3K+-$77+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+! (%"),
+          peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\xB7+#%'\"%$\"# X\"# X,8&!7B+-$7\xB7+#%'\"%$\"# X\"# X\"+'%4\"6\u0122\" %$\"# X\"# X"),
+          peg$decode("7\xB8*# \"7\x9F"),
+          peg$decode("!/\u0123\"\"1#3\u0124+<$7<+2%76+(%4#6\u0125#! %$## X$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u0126!! %"),
+          peg$decode("!7\x9D+' 4!6\u0127!! %"),
+          peg$decode("! \\7\x99,#&7\x99\"+\x81$7@+w%7M+m%7?+c% \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4%6\u0128% %$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("7\xBD"),
+          peg$decode("!/\u0129\"\"1&3\u012A+s$7.+i%7\xC0+_% \\!7A+-$7\xC0+#%'\"%$\"# X\"# X,8&!7A+-$7\xC0+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X*# \"7\xBE"),
+          peg$decode("!76+s$7.+i%7\xBF+_% \\!7A+-$7\xBF+#%'\"%$\"# X\"# X,8&!7A+-$7\xBF+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("!76+=$7<+3%76*# \"7H+#%'#%$## X$\"# X\"# X"),
+          peg$decode("7\xC1*G \"7\xC3*A \"7\xC5*; \"7\xC7*5 \"7\xC8*/ \"7\xC9*) \"7\xCA*# \"7\xBF"),
+          peg$decode("!/\u012B\"\"1%3\u012C+7$7<+-%7\xC2+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!7I+' 4!6\u012D!! %"),
+          peg$decode("!/\u012E\"\"1&3\u012F+\xA5$7<+\x9B%7D+\x91%7\xC4+\x87% \\! \\7'+&$,#&7'\"\"\" X+-$7\xC4+#%'\"%$\"# X\"# X,G&! \\7'+&$,#&7'\"\"\" X+-$7\xC4+#%'\"%$\"# X\"# X\"+-%7E+#%'&%$&# X$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("7t*# \"7w"),
+          peg$decode("!/\u0130\"\"1%3\u0131+7$7<+-%7\xC6+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!7I+' 4!6\u0132!! %"),
+          peg$decode("!/\u0133\"\"1&3\u0134+<$7<+2%7I+(%4#6\u0135#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0136\"\"1%3\u0137+_$7<+U%!/\u0138\"\"1$3\u0139+& 4!6\u013A! %*4 \"!/\u013B\"\"1%3\u013C+& 4!6\u013D! %+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!/\u013E\"\"1)3\u013F+T$7<+J%/\u0140\"\"1#3\u0141*/ \"/\u0142\"\"1(3\u0143*# \"76+(%4#6\u0144#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0145\"\"1#3\u0146+\x9E$7<+\x94%7D+\x8A%!7\xCB+k$ \\!.D\"\"2D3E+-$7\xCB+#%'\"%$\"# X\"# X,>&!.D\"\"2D3E+-$7\xCB+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+-%7E+#%'%%$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!/\u0147\"\"1(3\u0148*/ \"/\u0149\"\"1$3\u014A*# \"76+' 4!6\u014B!! %"),
+          peg$decode("!76+_$ \\!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!7\xCE+K$7.+A%7\xCE+7%7.+-%7\x8F+#%'%%$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u014C!! %"),
+          peg$decode("!7\xD0+c$ \\!7A+-$7\xD0+#%'\"%$\"# X\"# X,8&!7A+-$7\xD0+#%'\"%$\"# X\"# X\"+'%4\"6\u014D\" %$\"# X\"# X"),
+          peg$decode("!7\x98+c$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u014E\" %$\"# X\"# X"),
+          peg$decode("!7L*T \"7\x98*N \"!7@*# \" [+=$7t+3%7?*# \" [+#%'#%$## X$\"# X\"# X+c$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u014F\" %$\"# X\"# X"),
+          peg$decode("!7\xD3+c$ \\!7B+-$7\xD4+#%'\"%$\"# X\"# X,8&!7B+-$7\xD4+#%'\"%$\"# X\"# X\"+'%4\"6\u0150\" %$\"# X\"# X"),
+          peg$decode("!7\x95+& 4!6\u0151! %"),
+          peg$decode("!/\u0152\"\"1(3\u0153+<$7<+2%76+(%4#6\u0154#! %$## X$\"# X\"# X*j \"!/\u0155\"\"1&3\u0156+<$7<+2%76+(%4#6\u0157#! %$## X$\"# X\"# X*: \"!/\u0158\"\"1*3\u0159+& 4!6\u015A! %*# \"7\x9F"),
+          peg$decode("!!76+o$ \\!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X,=&!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X\"+)%4\"6\u015C\"\"! %$\"# X\"# X*# \" [+' 4!6\u015D!! %"),
+          peg$decode("!7\xD7+_$ \\!7A+-$7\xD7+#%'\"%$\"# X\"# X,8&!7A+-$7\xD7+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!7\x98+_$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u015E!! %"),
+          peg$decode("!7\xDA+_$ \\!7B+-$7\xDB+#%'\"%$\"# X\"# X,8&!7B+-$7\xDB+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!/\u015F\"\"1&3\u0160*; \"/\u0161\"\"1'3\u0162*/ \"/\u0163\"\"1*3\u0164*# \"76+& 4!6\u0165! %"),
+          peg$decode("!/\u0166\"\"1&3\u0167+<$7<+2%7\xDC+(%4#6\u0168#! %$## X$\"# X\"# X*\x83 \"!/\xF6\"\"1'3\xF7+<$7<+2%7\x9D+(%4#6\u0169#! %$## X$\"# X\"# X*S \"!/\u016A\"\"1+3\u016B+<$7<+2%7\x9D+(%4#6\u016C#! %$## X$\"# X\"# X*# \"7\x9F"),
+          peg$decode("/\u016D\"\"1+3\u016E*k \"/\u016F\"\"1)3\u0170*_ \"/\u0171\"\"1(3\u0172*S \"/\u0173\"\"1'3\u0174*G \"/\u0175\"\"1&3\u0176*; \"/\u0177\"\"1*3\u0178*/ \"/\u0179\"\"1)3\u017A*# \"76"),
+          peg$decode("71*# \" ["),
+          peg$decode("!!76+o$ \\!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X,=&!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X\"+)%4\"6\u015C\"\"! %$\"# X\"# X*# \" [+' 4!6\u017B!! %"),
+          peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\xE0+#%'\"%$\"# X\"# X,8&!7B+-$7\xE0+#%'\"%$\"# X\"# X\"+'%4\"6\u017C\" %$\"# X\"# X"),
+          peg$decode("7\xB8*# \"7\x9F"),
+          peg$decode("!7\xE2+_$ \\!7A+-$7\xE2+#%'\"%$\"# X\"# X,8&!7A+-$7\xE2+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
+          peg$decode("!7\xE9+s$7.+i%7\xEC+_% \\!7B+-$7\xE3+#%'\"%$\"# X\"# X,8&!7B+-$7\xE3+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("7\xE4*; \"7\xE5*5 \"7\xE6*/ \"7\xE7*) \"7\xE8*# \"7\x9F"),
+          peg$decode("!/\u017D\"\"1#3\u017E+<$7<+2%7\xEF+(%4#6\u017F#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0180\"\"1%3\u0181+<$7<+2%7T+(%4#6\u0182#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0183\"\"1(3\u0184+B$7<+8%7\\*# \"7Y+(%4#6\u0185#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0186\"\"1&3\u0187+<$7<+2%76+(%4#6\u0188#! %$## X$\"# X\"# X"),
+          peg$decode("!/\u0189\"\"1%3\u018A+T$!7<+5$ \\7!,#&7!\"+#%'\"%$\"# X\"# X*# \" [+'%4\"6\u018B\" %$\"# X\"# X"),
+          peg$decode("!7\xEA+K$7;+A%76+7%7;+-%7\xEB+#%'%%$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!/\x95\"\"1#3\xD6*# \"76+' 4!6\u018C!! %"),
+          peg$decode("!/\xB4\"\"1#3\u018D*G \"/\xB6\"\"1#3\u018E*; \"/\xBA\"\"1#3\u018F*/ \"/\xB8\"\"1$3\u0190*# \"76+' 4!6\u0191!! %"),
+          peg$decode("!7\xED+H$!7C+-$7\xEE+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"),
+          peg$decode("!7U*) \"7\\*# \"7X+& 4!6\u0192! %"),
+          peg$decode("!!7!*# \" [+c$7!*# \" [+S%7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\u0193!! %"),
+          peg$decode("!!7!+C$7!*# \" [+3%7!*# \" [+#%'#%$## X$\"# X\"# X+' 4!6\u0194!! %"),
+          peg$decode("7\xBD"),
+          peg$decode("!7\x9D+d$ \\!7B+-$7\xF2+#%'\"%$\"# X\"# X,8&!7B+-$7\xF2+#%'\"%$\"# X\"# X\"+(%4\"6\u0195\"!!%$\"# X\"# X"),
+          peg$decode("7\xF3*# \"7\x9F"),
+          peg$decode("!.\u0196\"\"2\u01963\u0197+N$7<+D%.\u0198\"\"2\u01983\u0199*) \".\u019A\"\"2\u019A3\u019B+(%4#6\u019C#! %$## X$\"# X\"# X"),
+          peg$decode("!7\x9D+d$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+(%4\"6\u019D\"!!%$\"# X\"# X"),
+          peg$decode("!76+7$70+-%7\xF6+#%'#%$## X$\"# X\"# X"),
+          peg$decode(" \\72*) \"74*# \"7.,/&72*) \"74*# \"7.\""),
+          peg$decode(" \\7%,#&7%\""),
+          peg$decode("!7\xF9+=$.8\"\"2839+-%7\xFA+#%'#%$## X$\"# X\"# X"),
+          peg$decode("!/\u019E\"\"1%3\u019F*) \"/\u01A0\"\"1$3\u01A1+' 4!6\u01A2!! %"),
+          peg$decode("!7\xFB+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"),
+          peg$decode("!7\\*) \"7X*# \"7\x82+' 4!6\u01A3!! %"),
+          peg$decode("! \\7\xFD*) \"7-*# \"7\xFE,/&7\xFD*) \"7-*# \"7\xFE\"+! (%"),
+          peg$decode("7\"*S \"7!*M \".F\"\"2F3G*A \".J\"\"2J3K*5 \".H\"\"2H3I*) \".N\"\"2N3O"),
+          peg$decode(".L\"\"2L3M*\x95 \".B\"\"2B3C*\x89 \".<\"\"2<3=*} \".R\"\"2R3S*q \".T\"\"2T3U*e \".V\"\"2V3W*Y \".P\"\"2P3Q*M \".@\"\"2@3A*A \".D\"\"2D3E*5 \".2\"\"2233*) \".>\"\"2>3?"),
+          peg$decode("!7\u0100+h$.8\"\"2839+X%7\xFA+N%!.\u01A4\"\"2\u01A43\u01A5+-$7\xEB+#%'\"%$\"# X\"# X*# \" [+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("!/\u01A6\"\"1%3\u01A7*) \"/\u01A8\"\"1$3\u01A9+' 4!6\u01A2!! %"),
+          peg$decode("!7\xEB+Q$/\xB4\"\"1#3\xB5*7 \"/\xB6\"\"1#3\xB7*+ \" \\7+,#&7+\"+'%4\"6\u01AA\" %$\"# X\"# X"),
+          peg$decode("!7\u0104+\x8F$.F\"\"2F3G+%7\u0103+u%.F\"\"2F3G+e%7\u0103+[%.F\"\"2F3G+K%7\u0103+A%.F\"\"2F3G+1%7\u0105+'%4)6\u01AB) %$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X"),
+          peg$decode("!7#+A$7#+7%7#+-%7#+#%'$%$$# X$## X$\"# X\"# X"),
+          peg$decode("!7\u0103+-$7\u0103+#%'\"%$\"# X\"# X"),
+          peg$decode("!7\u0103+7$7\u0103+-%7\u0103+#%'#%$## X$\"# X\"# X")
+        ],
+
+        peg$currPos          = 0,
+        peg$reportedPos      = 0,
+        peg$cachedPos        = 0,
+        peg$cachedPosDetails = { line: 1, column: 1, seenCR: false },
+        peg$maxFailPos       = 0,
+        peg$maxFailExpected  = [],
+        peg$silentFails      = 0,
+
+        peg$result;
+
+    if ("startRule" in options) {
+      if (!(options.startRule in peg$startRuleIndices)) {
+        throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+      }
+
+      peg$startRuleIndex = peg$startRuleIndices[options.startRule];
+    }
+
+    function text() {
+      return input.substring(peg$reportedPos, peg$currPos);
+    }
+
+    function offset() {
+      return peg$reportedPos;
+    }
+
+    function line() {
+      return peg$computePosDetails(peg$reportedPos).line;
+    }
+
+    function column() {
+      return peg$computePosDetails(peg$reportedPos).column;
+    }
+
+    function expected(description) {
+      throw peg$buildException(
+        null,
+        [{ type: "other", description: description }],
+        peg$reportedPos
+      );
+    }
+
+    function error(message) {
+      throw peg$buildException(message, null, peg$reportedPos);
+    }
+
+    function peg$computePosDetails(pos) {
+      function advance(details, startPos, endPos) {
+        var p, ch;
+
+        for (p = startPos; p < endPos; p++) {
+          ch = input.charAt(p);
+          if (ch === "\n") {
+            if (!details.seenCR) { details.line++; }
+            details.column = 1;
+            details.seenCR = false;
+          } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
+            details.line++;
+            details.column = 1;
+            details.seenCR = true;
+          } else {
+            details.column++;
+            details.seenCR = false;
+          }
+        }
+      }
+
+      if (peg$cachedPos !== pos) {
+        if (peg$cachedPos > pos) {
+          peg$cachedPos = 0;
+          peg$cachedPosDetails = { line: 1, column: 1, seenCR: false };
+        }
+        advance(peg$cachedPosDetails, peg$cachedPos, pos);
+        peg$cachedPos = pos;
+      }
+
+      return peg$cachedPosDetails;
+    }
+
+    function peg$fail(expected) {
+      if (peg$currPos < peg$maxFailPos) { return; }
+
+      if (peg$currPos > peg$maxFailPos) {
+        peg$maxFailPos = peg$currPos;
+        peg$maxFailExpected = [];
+      }
+
+      peg$maxFailExpected.push(expected);
+    }
+
+    function peg$buildException(message, expected, pos) {
+      function cleanupExpected(expected) {
+        var i = 1;
+
+        expected.sort(function(a, b) {
+          if (a.description < b.description) {
+            return -1;
+          } else if (a.description > b.description) {
+            return 1;
+          } else {
+            return 0;
+          }
+        });
+
+        while (i < expected.length) {
+          if (expected[i - 1] === expected[i]) {
+            expected.splice(i, 1);
+          } else {
+            i++;
+          }
+        }
+      }
+
+      function buildMessage(expected, found) {
+        function stringEscape(s) {
+          function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }
+
+          return s
+            .replace(/\\/g,   '\\\\')
+            .replace(/"/g,    '\\"')
+            .replace(/\x08/g, '\\b')
+            .replace(/\t/g,   '\\t')
+            .replace(/\n/g,   '\\n')
+            .replace(/\f/g,   '\\f')
+            .replace(/\r/g,   '\\r')
+            .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
+            .replace(/[\x10-\x1F\x80-\xFF]/g,    function(ch) { return '\\x'  + hex(ch); })
+            .replace(/[\u0180-\u0FFF]/g,         function(ch) { return '\\u0' + hex(ch); })
+            .replace(/[\u1080-\uFFFF]/g,         function(ch) { return '\\u'  + hex(ch); });
+        }
+
+        var expectedDescs = new Array(expected.length),
+            expectedDesc, foundDesc, i;
+
+        for (i = 0; i < expected.length; i++) {
+          expectedDescs[i] = expected[i].description;
+        }
+
+        expectedDesc = expected.length > 1
+          ? expectedDescs.slice(0, -1).join(", ")
+              + " or "
+              + expectedDescs[expected.length - 1]
+          : expectedDescs[0];
+
+        foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input";
+
+        return "Expected " + expectedDesc + " but " + foundDesc + " found.";
+      }
+
+      var posDetails = peg$computePosDetails(pos),
+          found      = pos < input.length ? input.charAt(pos) : null;
+
+      if (expected !== null) {
+        cleanupExpected(expected);
+      }
+
+      return new SyntaxError(
+        message !== null ? message : buildMessage(expected, found),
+        expected,
+        found,
+        pos,
+        posDetails.line,
+        posDetails.column
+      );
+    }
+
+    function peg$decode(s) {
+      var bc = new Array(s.length), i;
+
+      for (i = 0; i < s.length; i++) {
+        bc[i] = s.charCodeAt(i) - 32;
+      }
+
+      return bc;
+    }
+
+    function peg$parseRule(index) {
+      var bc    = peg$bytecode[index],
+          ip    = 0,
+          ips   = [],
+          end   = bc.length,
+          ends  = [],
+          stack = [],
+          params, i;
+
+      function protect(object) {
+        return Object.prototype.toString.apply(object) === "[object Array]" ? [] : object;
+      }
+
+      while (true) {
+        while (ip < end) {
+          switch (bc[ip]) {
+            case 0:
+              stack.push(protect(peg$consts[bc[ip + 1]]));
+              ip += 2;
+              break;
+
+            case 1:
+              stack.push(peg$currPos);
+              ip++;
+              break;
+
+            case 2:
+              stack.pop();
+              ip++;
+              break;
+
+            case 3:
+              peg$currPos = stack.pop();
+              ip++;
+              break;
+
+            case 4:
+              stack.length -= bc[ip + 1];
+              ip += 2;
+              break;
+
+            case 5:
+              stack.splice(-2, 1);
+              ip++;
+              break;
+
+            case 6:
+              stack[stack.length - 2].push(stack.pop());
+              ip++;
+              break;
+
+            case 7:
+              stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1]));
+              ip += 2;
+              break;
+
+            case 8:
+              stack.pop();
+              stack.push(input.substring(stack[stack.length - 1], peg$currPos));
+              ip++;
+              break;
+
+            case 9:
+              ends.push(end);
+              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);
+
+              if (stack[stack.length - 1]) {
+                end = ip + 3 + bc[ip + 1];
+                ip += 3;
+              } else {
+                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
+                ip += 3 + bc[ip + 1];
+              }
+
+              break;
+
+            case 10:
+              ends.push(end);
+              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);
+
+              if (stack[stack.length - 1] === peg$FAILED) {
+                end = ip + 3 + bc[ip + 1];
+                ip += 3;
+              } else {
+                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
+                ip += 3 + bc[ip + 1];
+              }
+
+              break;
+
+            case 11:
+              ends.push(end);
+              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);
+
+              if (stack[stack.length - 1] !== peg$FAILED) {
+                end = ip + 3 + bc[ip + 1];
+                ip += 3;
+              } else {
+                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
+                ip += 3 + bc[ip + 1];
+              }
+
+              break;
+
+            case 12:
+              if (stack[stack.length - 1] !== peg$FAILED) {
+                ends.push(end);
+                ips.push(ip);
+
+                end = ip + 2 + bc[ip + 1];
+                ip += 2;
+              } else {
+                ip += 2 + bc[ip + 1];
+              }
+
+              break;
+
+            case 13:
+              ends.push(end);
+              ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]);
+
+              if (input.length > peg$currPos) {
+                end = ip + 3 + bc[ip + 1];
+                ip += 3;
+              } else {
+                end = ip + 3 + bc[ip + 1] + bc[ip + 2];
+                ip += 3 + bc[ip + 1];
+              }
+
+              break;
+
+            case 14:
+              ends.push(end);
+              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);
+
+              if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]) {
+                end = ip + 4 + bc[ip + 2];
+                ip += 4;
+              } else {
+                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
+                ip += 4 + bc[ip + 2];
+              }
+
+              break;
+
+            case 15:
+              ends.push(end);
+              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);
+
+              if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]) {
+                end = ip + 4 + bc[ip + 2];
+                ip += 4;
+              } else {
+                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
+                ip += 4 + bc[ip + 2];
+              }
+
+              break;
+
+            case 16:
+              ends.push(end);
+              ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]);
+
+              if (peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))) {
+                end = ip + 4 + bc[ip + 2];
+                ip += 4;
+              } else {
+                end = ip + 4 + bc[ip + 2] + bc[ip + 3];
+                ip += 4 + bc[ip + 2];
+              }
+
+              break;
+
+            case 17:
+              stack.push(input.substr(peg$currPos, bc[ip + 1]));
+              peg$currPos += bc[ip + 1];
+              ip += 2;
+              break;
+
+            case 18:
+              stack.push(peg$consts[bc[ip + 1]]);
+              peg$currPos += peg$consts[bc[ip + 1]].length;
+              ip += 2;
+              break;
+
+            case 19:
+              stack.push(peg$FAILED);
+              if (peg$silentFails === 0) {
+                peg$fail(peg$consts[bc[ip + 1]]);
+              }
+              ip += 2;
+              break;
+
+            case 20:
+              peg$reportedPos = stack[stack.length - 1 - bc[ip + 1]];
+              ip += 2;
+              break;
+
+            case 21:
+              peg$reportedPos = peg$currPos;
+              ip++;
+              break;
+
+            case 22:
+              params = bc.slice(ip + 4, ip + 4 + bc[ip + 3]);
+              for (i = 0; i < bc[ip + 3]; i++) {
+                params[i] = stack[stack.length - 1 - params[i]];
+              }
+
+              stack.splice(
+                stack.length - bc[ip + 2],
+                bc[ip + 2],
+                peg$consts[bc[ip + 1]].apply(null, params)
+              );
+
+              ip += 4 + bc[ip + 3];
+              break;
+
+            case 23:
+              stack.push(peg$parseRule(bc[ip + 1]));
+              ip += 2;
+              break;
+
+            case 24:
+              peg$silentFails++;
+              ip++;
+              break;
+
+            case 25:
+              peg$silentFails--;
+              ip++;
+              break;
+
+            default:
+              throw new Error("Invalid opcode: " + bc[ip] + ".");
+          }
+        }
+
+        if (ends.length > 0) {
+          end = ends.pop();
+          ip = ips.pop();
+        } else {
+          break;
+        }
+      }
+
+      return stack[0];
+    }
+
+
+      options.data = {}; // Object to which header attributes will be assigned during parsing
+
+      function list (first, rest) {
+        return [first].concat(rest);
+      }
+
+
+    peg$result = peg$parseRule(peg$startRuleIndex);
+
+    if (peg$result !== peg$FAILED && peg$currPos === input.length) {
+      return peg$result;
+    } else {
+      if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+        peg$fail({ type: "end", description: "end of input" });
+      }
+
+      throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos);
+    }
+  }
+
+  return {
+    SyntaxError: SyntaxError,
+    parse:       parse
+  };
+})();
+},{}],12:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview Hacks - This file contains all of the things we
+ * wish we didn't have to do, just for interop.  It is similar to
+ * Utils, which provides actually useful and relevant functions for
+ * a SIP library. Methods in this file are grouped by vendor, so
+ * as to most easily track when particular hacks may not be necessary anymore.
+ */
+
+module.exports = function (SIP) {
+
+//keep to quiet jshint, and remain consistent with other files
+SIP = SIP;
+
+var Hacks = {
+  AllBrowsers: {
+    maskDtls: function (sdp) {
+      if (sdp) {
+        sdp = sdp.replace(/ UDP\/TLS\/RTP\/SAVP/gmi, " RTP/SAVP");
+      }
+      return sdp;
+    },
+    unmaskDtls: function (sdp) {
+      /**
+       * Chrome does not handle DTLS correctly (Canaray does, but not production)
+       * keeping Chrome as SDES until DTLS is fixed (comment out 'is_opera' condition)
+       *
+       * UPDATE: May 21, 2014
+       * Chrome 35 now properly defaults to DTLS.  Only Opera remains using SDES
+       *
+       * UPDATE: 2014-09-24
+       * Opera now supports DTLS by default as well.
+       *
+       **/
+      return sdp.replace(/ RTP\/SAVP/gmi, " UDP/TLS/RTP/SAVP");
+    }
+  },
+  Firefox: {
+    /* Condition to detect if hacks are applicable */
+    isFirefox: function () {
+      return typeof mozRTCPeerConnection !== 'undefined';
+    },
+
+    cannotHandleExtraWhitespace: function (sdp) {
+      if (this.isFirefox() && sdp) {
+        sdp = sdp.replace(/ \r\n/g, "\r\n");
+      }
+      return sdp;
+    },
+
+    hasMissingCLineInSDP: function (sdp) {
+      /*
+       * This is a Firefox hack to insert valid sdp when getDescription is
+       * called with the constraint offerToReceiveVideo = false.
+       * We search for either a c-line at the top of the sdp above all
+       * m-lines. If that does not exist then we search for a c-line
+       * beneath each m-line. If it is missing a c-line, we insert
+       * a fake c-line with the ip address 0.0.0.0. This is then valid
+       * sdp and no media will be sent for that m-line.
+       *
+       * Valid SDP is:
+       * m=
+       * i=
+       * c=
+       */
+      var insertAt, mlines;
+      if (sdp.indexOf('c=') > sdp.indexOf('m=')) {
+
+        // Find all m= lines
+        mlines = sdp.match(/m=.*\r\n.*/g);
+        for (var i=0; i<mlines.length; i++) {
+
+          // If it has an i= line, check if the next line is the c= line
+          if (mlines[i].toString().search(/i=.*/) >= 0) {
+            insertAt = sdp.indexOf(mlines[i].toString())+mlines[i].toString().length;
+            if (sdp.substr(insertAt,2)!=='c=') {
+              sdp = sdp.substr(0,insertAt) + '\r\nc=IN IP4 0.0.0.0' + sdp.substr(insertAt);
+            }
+
+          // else add the C line if it's missing
+          } else if (mlines[i].toString().search(/c=.*/) < 0) {
+            insertAt = sdp.indexOf(mlines[i].toString().match(/.*/))+mlines[i].toString().match(/.*/).toString().length;
+            sdp = sdp.substr(0,insertAt) + '\r\nc=IN IP4 0.0.0.0' + sdp.substr(insertAt);
+          }
+        }
+      }
+      return sdp;
+    },
+  },
+
+  Chrome: {
+    needsExplicitlyInactiveSDP: function (sdp) {
+      var sub, index;
+
+      if (Hacks.Firefox.isFirefox()) { // Fix this in Firefox before sending
+        index = sdp.indexOf('m=video 0');
+        if (index !== -1) {
+          sub = sdp.substr(index);
+          sub = sub.replace(/\r\nc=IN IP4.*\r\n$/,
+                            '\r\nc=IN IP4 0.0.0.0\r\na=inactive\r\n');
+          return sdp.substr(0, index) + sub;
+        }
+      }
+      return sdp;
+    },
+
+    getsConfusedAboutGUM: function (session) {
+      if (session.mediaHandler) {
+        session.mediaHandler.close();
+      }
+    }
+  }
+};
+return Hacks;
+};
+
+},{}],13:[function(require,module,exports){
+"use strict";
+var levels = {
+  'error': 0,
+  'warn': 1,
+  'log': 2,
+  'debug': 3
+};
+
+module.exports = function (console) {
+
+var LoggerFactory = function () {
+  var logger,
+    level = 2,
+    builtinEnabled = true,
+    connector = null;
+
+    this.loggers = {};
+
+    logger = this.getLogger('sip.loggerfactory');
+
+
+  Object.defineProperties(this, {
+    builtinEnabled: {
+      get: function(){ return builtinEnabled; },
+      set: function(value){
+        if (typeof value === 'boolean') {
+          builtinEnabled = value;
+        } else {
+          logger.error('invalid "builtinEnabled" parameter value: '+ JSON.stringify(value));
+        }
+      }
+    },
+
+    level: {
+      get: function() {return level; },
+      set: function(value) {
+        if (value >= 0 && value <=3) {
+          level = value;
+        } else if (value > 3) {
+          level = 3;
+        } else if (levels.hasOwnProperty(value)) {
+          level = levels[value];
+        } else {
+          logger.error('invalid "level" parameter value: '+ JSON.stringify(value));
+        }
+      }
+    },
+
+    connector: {
+      get: function() {return connector; },
+      set: function(value){
+        if(value === null || value === "" || value === undefined) {
+          connector = null;
+        } else if (typeof value === 'function') {
+          connector = value;
+        } else {
+          logger.error('invalid "connector" parameter value: '+ JSON.stringify(value));
+        }
+      }
+    }
+  });
+};
+
+LoggerFactory.prototype.print = function(target, category, label, content) {
+  if (typeof content === 'string') {
+    var prefix = [new Date(), category];
+    if (label) {
+      prefix.push(label);
+    }
+    content = prefix.concat(content).join(' | ');
+  }
+  target.call(console, content);
+};
+
+function Logger (logger, category, label) {
+  this.logger = logger;
+  this.category = category;
+  this.label = label;
+}
+
+Object.keys(levels).forEach(function (targetName) {
+  Logger.prototype[targetName] = function (content) {
+    this.logger[targetName](this.category, this.label, content);
+  };
+
+  LoggerFactory.prototype[targetName] = function (category, label, content) {
+    if (this.level >= levels[targetName]) {
+      if (this.builtinEnabled) {
+        this.print(console[targetName], category, label, content);
+      }
+
+      if (this.connector) {
+        this.connector(targetName, category, label, content);
+      }
+    }
+  };
+});
+
+LoggerFactory.prototype.getLogger = function(category, label) {
+  var logger;
+
+  if (label && this.level === 3) {
+    return new Logger(this, category, label);
+  } else if (this.loggers[category]) {
+    return this.loggers[category];
+  } else {
+    logger = new Logger(this, category);
+    this.loggers[category] = logger;
+    return logger;
+  }
+};
+
+return LoggerFactory;
+};
+
+},{}],14:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview MediaHandler
+ */
+
+/* MediaHandler
+ * @class PeerConnection helper Class.
+ * @param {SIP.Session} session
+ * @param {Object} [options]
+ */
+module.exports = function (EventEmitter) {
+var MediaHandler = function(session, options) {
+  // keep jshint happy
+  session = session;
+  options = options;
+};
+
+MediaHandler.prototype = Object.create(EventEmitter.prototype, {
+  isReady: {value: function isReady () {}},
+
+  close: {value: function close () {}},
+
+  /**
+   * @param {Object} [mediaHint] A custom object describing the media to be used during this session.
+   */
+  getDescription: {value: function getDescription (mediaHint) {
+    // keep jshint happy
+    mediaHint = mediaHint;
+  }},
+
+  /**
+   * Check if a SIP message contains a session description.
+   * @param {SIP.SIPMessage} message
+   * @returns {boolean}
+   */
+  hasDescription: {value: function hasDescription (message) {
+    // keep jshint happy
+    message = message;
+  }},
+
+  /**
+   * Set the session description contained in a SIP message.
+   * @param {SIP.SIPMessage} message
+   * @returns {Promise}
+   */
+  setDescription: {value: function setDescription (message) {
+    // keep jshint happy
+    message = message;
+  }}
+});
+
+return MediaHandler;
+};
+
+},{}],15:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP NameAddrHeader
+ */
+
+/**
+ * @augments SIP
+ * @class Class creating a Name Address SIP header.
+ *
+ * @param {SIP.URI} uri
+ * @param {String} [displayName]
+ * @param {Object} [parameters]
+ *
+ */
+module.exports = function (SIP) {
+var NameAddrHeader;
+
+NameAddrHeader = function(uri, displayName, parameters) {
+  var param;
+
+  // Checks
+  if(!uri || !(uri instanceof SIP.URI)) {
+    throw new TypeError('missing or invalid "uri" parameter');
+  }
+
+  // Initialize parameters
+  this.uri = uri;
+  this.parameters = {};
+
+  for (param in parameters) {
+    this.setParam(param, parameters[param]);
+  }
+
+  Object.defineProperties(this, {
+    friendlyName: {
+      get: function() { return this.displayName || uri.aor; }
+    },
+
+    displayName: {
+      get: function() { return displayName; },
+      set: function(value) {
+        displayName = (value === 0) ? '0' : value;
+      }
+    }
+  });
+};
+NameAddrHeader.prototype = {
+  setParam: function (key, value) {
+    if(key) {
+      this.parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString();
+    }
+  },
+  getParam: SIP.URI.prototype.getParam,
+  hasParam: SIP.URI.prototype.hasParam,
+  deleteParam: SIP.URI.prototype.deleteParam,
+  clearParams: SIP.URI.prototype.clearParams,
+
+  clone: function() {
+    return new NameAddrHeader(
+      this.uri.clone(),
+      this.displayName,
+      JSON.parse(JSON.stringify(this.parameters)));
+  },
+
+  toString: function() {
+    var body, parameter;
+
+    body  = (this.displayName || this.displayName === 0) ? '"' + this.displayName + '" ' : '';
+    body += '<' + this.uri.toString() + '>';
+
+    for (parameter in this.parameters) {
+      body += ';' + parameter;
+
+      if (this.parameters[parameter] !== null) {
+        body += '='+ this.parameters[parameter];
+      }
+    }
+
+    return body;
+  }
+};
+
+
+/**
+  * Parse the given string and returns a SIP.NameAddrHeader instance or undefined if
+  * it is an invalid NameAddrHeader.
+  * @public
+  * @param {String} name_addr_header
+  */
+NameAddrHeader.parse = function(name_addr_header) {
+  name_addr_header = SIP.Grammar.parse(name_addr_header,'Name_Addr_Header');
+
+  if (name_addr_header !== -1) {
+    return name_addr_header;
+  } else {
+    return undefined;
+  }
+};
+
+SIP.NameAddrHeader = NameAddrHeader;
+};
+
+},{}],16:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP Message Parser
+ */
+
+/**
+ * Extract and parse every header of a SIP message.
+ * @augments SIP
+ * @namespace
+ */
+module.exports = function (SIP) {
+var Parser;
+
+function getHeader(data, headerStart) {
+  var
+    // 'start' position of the header.
+    start = headerStart,
+    // 'end' position of the header.
+    end = 0,
+    // 'partial end' position of the header.
+    partialEnd = 0;
+
+  //End of message.
+  if (data.substring(start, start + 2).match(/(^\r\n)/)) {
+    return -2;
+  }
+
+  while(end === 0) {
+    // Partial End of Header.
+    partialEnd = data.indexOf('\r\n', start);
+
+    // 'indexOf' returns -1 if the value to be found never occurs.
+    if (partialEnd === -1) {
+      return partialEnd;
+    }
+
+    if(!data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) && data.charAt(partialEnd + 2).match(/(^\s+)/)) {
+      // Not the end of the message. Continue from the next position.
+      start = partialEnd + 2;
+    } else {
+      end = partialEnd;
+    }
+  }
+
+  return end;
+}
+
+function parseHeader(message, data, headerStart, headerEnd) {
+  var header, idx, length, parsed,
+    hcolonIndex = data.indexOf(':', headerStart),
+    headerName = data.substring(headerStart, hcolonIndex).trim(),
+    headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
+
+  // If header-field is well-known, parse it.
+  switch(headerName.toLowerCase()) {
+    case 'via':
+    case 'v':
+      message.addHeader('via', headerValue);
+      if(message.getHeaders('via').length === 1) {
+        parsed = message.parseHeader('Via');
+        if(parsed) {
+          message.via = parsed;
+          message.via_branch = parsed.branch;
+        }
+      } else {
+        parsed = 0;
+      }
+      break;
+    case 'from':
+    case 'f':
+      message.setHeader('from', headerValue);
+      parsed = message.parseHeader('from');
+      if(parsed) {
+        message.from = parsed;
+        message.from_tag = parsed.getParam('tag');
+      }
+      break;
+    case 'to':
+    case 't':
+      message.setHeader('to', headerValue);
+      parsed = message.parseHeader('to');
+      if(parsed) {
+        message.to = parsed;
+        message.to_tag = parsed.getParam('tag');
+      }
+      break;
+    case 'record-route':
+      parsed = SIP.Grammar.parse(headerValue, 'Record_Route');
+
+      if (parsed === -1) {
+        parsed = undefined;
+        break;
+      }
+
+      length = parsed.length;
+      for (idx = 0; idx < length; idx++) {
+        header = parsed[idx];
+        message.addHeader('record-route', headerValue.substring(header.position, header.offset));
+        message.headers['Record-Route'][message.getHeaders('record-route').length - 1].parsed = header.parsed;
+      }
+      break;
+    case 'call-id':
+    case 'i':
+      message.setHeader('call-id', headerValue);
+      parsed = message.parseHeader('call-id');
+      if(parsed) {
+        message.call_id = headerValue;
+      }
+      break;
+    case 'contact':
+    case 'm':
+      parsed = SIP.Grammar.parse(headerValue, 'Contact');
+
+      if (parsed === -1) {
+        parsed = undefined;
+        break;
+      }
+
+      length = parsed.length;
+      for (idx = 0; idx < length; idx++) {
+        header = parsed[idx];
+        message.addHeader('contact', headerValue.substring(header.position, header.offset));
+        message.headers['Contact'][message.getHeaders('contact').length - 1].parsed = header.parsed;
+      }
+      break;
+    case 'content-length':
+    case 'l':
+      message.setHeader('content-length', headerValue);
+      parsed = message.parseHeader('content-length');
+      break;
+    case 'content-type':
+    case 'c':
+      message.setHeader('content-type', headerValue);
+      parsed = message.parseHeader('content-type');
+      break;
+    case 'cseq':
+      message.setHeader('cseq', headerValue);
+      parsed = message.parseHeader('cseq');
+      if(parsed) {
+        message.cseq = parsed.value;
+      }
+      if(message instanceof SIP.IncomingResponse) {
+        message.method = parsed.method;
+      }
+      break;
+    case 'max-forwards':
+      message.setHeader('max-forwards', headerValue);
+      parsed = message.parseHeader('max-forwards');
+      break;
+    case 'www-authenticate':
+      message.setHeader('www-authenticate', headerValue);
+      parsed = message.parseHeader('www-authenticate');
+      break;
+    case 'proxy-authenticate':
+      message.setHeader('proxy-authenticate', headerValue);
+      parsed = message.parseHeader('proxy-authenticate');
+      break;
+    case 'refer-to':
+    case 'r':
+      message.setHeader('refer-to', headerValue);
+      parsed = message.parseHeader('refer-to');
+      if (parsed) {
+        message.refer_to = parsed;
+      }
+      break;
+    default:
+      // Do not parse this header.
+      message.setHeader(headerName, headerValue);
+      parsed = 0;
+  }
+
+  if (parsed === undefined) {
+    return {
+      error: 'error parsing header "'+ headerName +'"'
+    };
+  } else {
+    return true;
+  }
+}
+
+/** Parse SIP Message
+ * @function
+ * @param {String} message SIP message.
+ * @param {Object} logger object.
+ * @returns {SIP.IncomingRequest|SIP.IncomingResponse|undefined}
+ */
+Parser = {};
+Parser.parseMessage = function(data, ua) {
+  var message, firstLine, contentLength, bodyStart, parsed,
+    headerStart = 0,
+    headerEnd = data.indexOf('\r\n'),
+    logger = ua.getLogger('sip.parser');
+
+  if(headerEnd === -1) {
+    logger.warn('no CRLF found, not a SIP message, discarded');
+    return;
+  }
+
+  // Parse first line. Check if it is a Request or a Reply.
+  firstLine = data.substring(0, headerEnd);
+  parsed = SIP.Grammar.parse(firstLine, 'Request_Response');
+
+  if(parsed === -1) {
+    logger.warn('error parsing first line of SIP message: "' + firstLine + '"');
+    return;
+  } else if(!parsed.status_code) {
+    message = new SIP.IncomingRequest(ua);
+    message.method = parsed.method;
+    message.ruri = parsed.uri;
+  } else {
+    message = new SIP.IncomingResponse(ua);
+    message.status_code = parsed.status_code;
+    message.reason_phrase = parsed.reason_phrase;
+  }
+
+  message.data = data;
+  headerStart = headerEnd + 2;
+
+  /* Loop over every line in data. Detect the end of each header and parse
+  * it or simply add to the headers collection.
+  */
+  while(true) {
+    headerEnd = getHeader(data, headerStart);
+
+    // The SIP message has normally finished.
+    if(headerEnd === -2) {
+      bodyStart = headerStart + 2;
+      break;
+    }
+    // data.indexOf returned -1 due to a malformed message.
+    else if(headerEnd === -1) {
+      logger.error('malformed message');
+      return;
+    }
+
+    parsed = parseHeader(message, data, headerStart, headerEnd);
+
+    if(parsed !== true) {
+      logger.error(parsed.error);
+      return;
+    }
+
+    headerStart = headerEnd + 2;
+  }
+
+  /* RFC3261 18.3.
+   * If there are additional bytes in the transport packet
+   * beyond the end of the body, they MUST be discarded.
+   */
+  if(message.hasHeader('content-length')) {
+    contentLength = message.getHeader('content-length');
+    message.body = data.substr(bodyStart, contentLength);
+  } else {
+    message.body = data.substring(bodyStart);
+  }
+
+  return message;
+};
+
+SIP.Parser = Parser;
+};
+
+},{}],17:[function(require,module,exports){
+"use strict";
+module.exports = function (SIP) {
+
+var RegisterContext;
+
+RegisterContext = function (ua) {
+  var params = {},
+      regId = 1;
+
+  this.registrar = ua.configuration.registrarServer;
+  this.expires = ua.configuration.registerExpires;
+
+
+  // Contact header
+  this.contact = ua.contact.toString();
+
+  if(regId) {
+    this.contact += ';reg-id='+ regId;
+    this.contact += ';+sip.instance="<urn:uuid:'+ ua.configuration.instanceId+'>"';
+  }
+
+  // Call-ID and CSeq values RFC3261 10.2
+  this.call_id = SIP.Utils.createRandomToken(22);
+  this.cseq = 80;
+
+  this.to_uri = ua.configuration.uri;
+
+  params.to_uri = this.to_uri;
+  params.to_displayName = ua.configuration.displayName;
+  params.call_id = this.call_id;
+  params.cseq = this.cseq;
+
+  // Extends ClientContext
+  SIP.Utils.augment(this, SIP.ClientContext, [ua, 'REGISTER', this.registrar, {params: params}]);
+
+  this.registrationTimer = null;
+  this.registrationExpiredTimer = null;
+
+  // Set status
+  this.registered = false;
+
+  this.logger = ua.getLogger('sip.registercontext');
+};
+
+RegisterContext.prototype = {
+  register: function (options) {
+    var self = this, extraHeaders;
+
+    // Handle Options
+    this.options = options || {};
+    extraHeaders = (this.options.extraHeaders || []).slice();
+    extraHeaders.push('Contact: ' + this.contact + ';expires=' + this.expires);
+    extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString());
+
+    // Save original extraHeaders to be used in .close
+    this.closeHeaders = this.options.closeWithHeaders ?
+      (this.options.extraHeaders || []).slice() : [];
+
+    this.receiveResponse = function(response) {
+      var contact, expires,
+        contacts = response.getHeaders('contact').length,
+        cause;
+
+      // Discard responses to older REGISTER/un-REGISTER requests.
+      if(response.cseq !== this.cseq) {
+        return;
+      }
+
+      // Clear registration timer
+      if (this.registrationTimer !== null) {
+        SIP.Timers.clearTimeout(this.registrationTimer);
+        this.registrationTimer = null;
+      }
+
+      switch(true) {
+        case /^1[0-9]{2}$/.test(response.status_code):
+          this.emit('progress', response);
+          break;
+        case /^2[0-9]{2}$/.test(response.status_code):
+          this.emit('accepted', response);
+
+          if(response.hasHeader('expires')) {
+            expires = response.getHeader('expires');
+          }
+
+          if (this.registrationExpiredTimer !== null) {
+            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
+            this.registrationExpiredTimer = null;
+          }
+
+          // Search the Contact pointing to us and update the expires value accordingly.
+          if (!contacts) {
+            this.logger.warn('no Contact header in response to REGISTER, response ignored');
+            break;
+          }
+
+          while(contacts--) {
+            contact = response.parseHeader('contact', contacts);
+            if(contact.uri.user === this.ua.contact.uri.user) {
+              expires = contact.getParam('expires');
+              break;
+            } else {
+              contact = null;
+            }
+          }
+
+          if (!contact) {
+            this.logger.warn('no Contact header pointing to us, response ignored');
+            break;
+          }
+
+          if(!expires) {
+            expires = this.expires;
+          }
+
+          // Re-Register before the expiration interval has elapsed.
+          // For that, decrease the expires value. ie: 3 seconds
+          this.registrationTimer = SIP.Timers.setTimeout(function() {
+            self.registrationTimer = null;
+            self.register(self.options);
+          }, (expires * 1000) - 3000);
+          this.registrationExpiredTimer = SIP.Timers.setTimeout(function () {
+            self.logger.warn('registration expired');
+            if (self.registered) {
+              self.unregistered(null, SIP.C.causes.EXPIRES);
+            }
+          }, expires * 1000);
+
+          //Save gruu values
+          if (contact.hasParam('temp-gruu')) {
+            this.ua.contact.temp_gruu = SIP.URI.parse(contact.getParam('temp-gruu').replace(/"/g,''));
+          }
+          if (contact.hasParam('pub-gruu')) {
+            this.ua.contact.pub_gruu = SIP.URI.parse(contact.getParam('pub-gruu').replace(/"/g,''));
+          }
+
+          this.registered = true;
+          this.emit('registered', response || null);
+          break;
+        // Interval too brief RFC3261 10.2.8
+        case /^423$/.test(response.status_code):
+          if(response.hasHeader('min-expires')) {
+            // Increase our registration interval to the suggested minimum
+            this.expires = response.getHeader('min-expires');
+            // Attempt the registration again immediately
+            this.register(this.options);
+          } else { //This response MUST contain a Min-Expires header field
+            this.logger.warn('423 response received for REGISTER without Min-Expires');
+            this.registrationFailure(response, SIP.C.causes.SIP_FAILURE_CODE);
+          }
+          break;
+        default:
+          cause = SIP.Utils.sipErrorCause(response.status_code);
+          this.registrationFailure(response, cause);
+      }
+    };
+
+    this.onRequestTimeout = function() {
+      this.registrationFailure(null, SIP.C.causes.REQUEST_TIMEOUT);
+    };
+
+    this.onTransportError = function() {
+      this.registrationFailure(null, SIP.C.causes.CONNECTION_ERROR);
+    };
+
+    this.cseq++;
+    this.request.cseq = this.cseq;
+    this.request.setHeader('cseq', this.cseq + ' REGISTER');
+    this.request.extraHeaders = extraHeaders;
+    this.send();
+  },
+
+  registrationFailure: function (response, cause) {
+    this.emit('failed', response || null, cause || null);
+  },
+
+  onTransportClosed: function() {
+    this.registered_before = this.registered;
+    if (this.registrationTimer !== null) {
+      SIP.Timers.clearTimeout(this.registrationTimer);
+      this.registrationTimer = null;
+    }
+
+    if (this.registrationExpiredTimer !== null) {
+      SIP.Timers.clearTimeout(this.registrationExpiredTimer);
+      this.registrationExpiredTimer = null;
+    }
+
+    if(this.registered) {
+      this.unregistered(null, SIP.C.causes.CONNECTION_ERROR);
+    }
+  },
+
+  onTransportConnected: function() {
+    this.register(this.options);
+  },
+
+  close: function() {
+    var options = {
+      all: false,
+      extraHeaders: this.closeHeaders
+    };
+
+    this.registered_before = this.registered;
+    this.unregister(options);
+  },
+
+  unregister: function(options) {
+    var extraHeaders;
+
+    options = options || {};
+
+    if(!this.registered && !options.all) {
+      this.logger.warn('already unregistered');
+      return;
+    }
+
+    extraHeaders = (options.extraHeaders || []).slice();
+
+    this.registered = false;
+
+    // Clear the registration timer.
+    if (this.registrationTimer !== null) {
+      SIP.Timers.clearTimeout(this.registrationTimer);
+      this.registrationTimer = null;
+    }
+
+    if(options.all) {
+      extraHeaders.push('Contact: *');
+      extraHeaders.push('Expires: 0');
+    } else {
+      extraHeaders.push('Contact: '+ this.contact + ';expires=0');
+    }
+
+
+    this.receiveResponse = function(response) {
+      var cause;
+
+      switch(true) {
+        case /^1[0-9]{2}$/.test(response.status_code):
+          this.emit('progress', response);
+          break;
+        case /^2[0-9]{2}$/.test(response.status_code):
+          this.emit('accepted', response);
+          if (this.registrationExpiredTimer !== null) {
+            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
+            this.registrationExpiredTimer = null;
+          }
+          this.unregistered(response);
+          break;
+        default:
+          cause = SIP.Utils.sipErrorCause(response.status_code);
+          this.unregistered(response,cause);
+      }
+    };
+
+    this.onRequestTimeout = function() {
+      // Not actually unregistered...
+      //this.unregistered(null, SIP.C.causes.REQUEST_TIMEOUT);
+    };
+
+    this.onTransportError = function() {
+      // Not actually unregistered...
+      //this.unregistered(null, SIP.C.causes.CONNECTION_ERROR);
+    };
+
+    this.cseq++;
+    this.request.cseq = this.cseq;
+    this.request.setHeader('cseq', this.cseq + ' REGISTER');
+    this.request.extraHeaders = extraHeaders;
+
+    this.send();
+  },
+
+  unregistered: function(response, cause) {
+    this.registered = false;
+    this.emit('unregistered', response || null, cause || null);
+  }
+
+};
+
+
+SIP.RegisterContext = RegisterContext;
+};
+
+},{}],18:[function(require,module,exports){
+"use strict";
+
+/**
+ * @fileoverview Request Sender
+ */
+
+/**
+ * @augments SIP
+ * @class Class creating a request sender.
+ * @param {Object} applicant
+ * @param {SIP.UA} ua
+ */
+module.exports = function (SIP) {
+var RequestSender;
+
+RequestSender = function(applicant, ua) {
+  this.logger = ua.getLogger('sip.requestsender');
+  this.ua = ua;
+  this.applicant = applicant;
+  this.method = applicant.request.method;
+  this.request = applicant.request;
+  this.credentials = null;
+  this.challenged = false;
+  this.staled = false;
+
+  // If ua is in closing process or even closed just allow sending Bye and ACK
+  if (ua.status === SIP.UA.C.STATUS_USER_CLOSED && (this.method !== SIP.C.BYE || this.method !== SIP.C.ACK)) {
+    this.onTransportError();
+  }
+};
+
+/**
+* Create the client transaction and send the message.
+*/
+RequestSender.prototype = {
+  send: function() {
+    switch(this.method) {
+      case "INVITE":
+        this.clientTransaction = new SIP.Transactions.InviteClientTransaction(this, this.request, this.ua.transport);
+        break;
+      case "ACK":
+        this.clientTransaction = new SIP.Transactions.AckClientTransaction(this, this.request, this.ua.transport);
+        break;
+      default:
+        this.clientTransaction = new SIP.Transactions.NonInviteClientTransaction(this, this.request, this.ua.transport);
+    }
+    this.clientTransaction.send();
+
+    return this.clientTransaction;
+  },
+
+  /**
+  * Callback fired when receiving a request timeout error from the client transaction.
+  * To be re-defined by the applicant.
+  * @event
+  */
+  onRequestTimeout: function() {
+    this.applicant.onRequestTimeout();
+  },
+
+  /**
+  * Callback fired when receiving a transport error from the client transaction.
+  * To be re-defined by the applicant.
+  * @event
+  */
+  onTransportError: function() {
+    this.applicant.onTransportError();
+  },
+
+  /**
+  * Called from client transaction when receiving a correct response to the request.
+  * Authenticate request if needed or pass the response back to the applicant.
+  * @param {SIP.IncomingResponse} response
+  */
+  receiveResponse: function(response) {
+    var cseq, challenge, authorization_header_name,
+      status_code = response.status_code;
+
+    /*
+    * Authentication
+    * Authenticate once. _challenged_ flag used to avoid infinite authentications.
+    */
+    if (status_code === 401 || status_code === 407) {
+
+      // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
+      if (response.status_code === 401) {
+        challenge = response.parseHeader('www-authenticate');
+        authorization_header_name = 'authorization';
+      } else {
+        challenge = response.parseHeader('proxy-authenticate');
+        authorization_header_name = 'proxy-authorization';
+      }
+
+      // Verify it seems a valid challenge.
+      if (! challenge) {
+        this.logger.warn(response.status_code + ' with wrong or missing challenge, cannot authenticate');
+        this.applicant.receiveResponse(response);
+        return;
+      }
+
+      if (!this.challenged || (!this.staled && challenge.stale === true)) {
+        if (!this.credentials) {
+          this.credentials = this.ua.configuration.authenticationFactory(this.ua);
+        }
+
+        // Verify that the challenge is really valid.
+        if (!this.credentials.authenticate(this.request, challenge)) {
+          this.applicant.receiveResponse(response);
+          return;
+        }
+        this.challenged = true;
+
+        if (challenge.stale) {
+          this.staled = true;
+        }
+
+        if (response.method === SIP.C.REGISTER) {
+          cseq = this.applicant.cseq += 1;
+        } else if (this.request.dialog){
+          cseq = this.request.dialog.local_seqnum += 1;
+        } else {
+          cseq = this.request.cseq + 1;
+          this.request.cseq = cseq;
+        }
+        this.request.setHeader('cseq', cseq +' '+ this.method);
+
+        this.request.setHeader(authorization_header_name, this.credentials.toString());
+        this.send();
+      } else {
+        this.applicant.receiveResponse(response);
+      }
+    } else {
+      this.applicant.receiveResponse(response);
+    }
+  }
+};
+
+SIP.RequestSender = RequestSender;
+};
+
+},{}],19:[function(require,module,exports){
+/**
+ * @name SIP
+ * @namespace
+ */
+"use strict";
+
+module.exports = function (environment) {
+
+var pkg = require('../package.json'),
+    version = pkg.version,
+    title = pkg.title;
+
+var SIP = Object.defineProperties({}, {
+  version: {
+    get: function(){ return version; }
+  },
+  name: {
+    get: function(){ return title; }
+  }
+});
+
+require('./Utils')(SIP, environment);
+SIP.LoggerFactory = require('./LoggerFactory')(environment.console);
+SIP.EventEmitter = require('./EventEmitter')(environment.console);
+SIP.C = require('./Constants')(SIP.name, SIP.version);
+SIP.Exceptions = require('./Exceptions');
+SIP.Timers = require('./Timers')(environment.timers);
+SIP.Transport = environment.Transport(SIP, environment.WebSocket);
+require('./Parser')(SIP);
+require('./SIPMessage')(SIP);
+require('./URI')(SIP);
+require('./NameAddrHeader')(SIP);
+require('./Transactions')(SIP);
+require('./Dialogs')(SIP);
+require('./RequestSender')(SIP);
+require('./RegisterContext')(SIP);
+SIP.MediaHandler = require('./MediaHandler')(SIP.EventEmitter);
+require('./ClientContext')(SIP);
+require('./ServerContext')(SIP);
+require('./Session')(SIP, environment);
+require('./Subscription')(SIP);
+SIP.WebRTC = require('./WebRTC')(SIP, environment);
+require('./UA')(SIP, environment);
+SIP.Hacks = require('./Hacks')(SIP);
+require('./SanityCheck')(SIP);
+SIP.DigestAuthentication = require('./DigestAuthentication')(SIP.Utils);
+SIP.Grammar = require('./Grammar')(SIP);
+
+return SIP;
+};
+
+},{"../package.json":2,"./ClientContext":3,"./Constants":4,"./Dialogs":6,"./DigestAuthentication":7,"./EventEmitter":8,"./Exceptions":9,"./Grammar":10,"./Hacks":12,"./LoggerFactory":13,"./MediaHandler":14,"./NameAddrHeader":15,"./Parser":16,"./RegisterContext":17,"./RequestSender":18,"./SIPMessage":20,"./SanityCheck":21,"./ServerContext":22,"./Session":23,"./Subscription":25,"./Timers":26,"./Transactions":27,"./UA":29,"./URI":30,"./Utils":31,"./WebRTC":32}],20:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP Message
+ */
+
+module.exports = function (SIP) {
+var
+  OutgoingRequest,
+  IncomingMessage,
+  IncomingRequest,
+  IncomingResponse;
+
+function getSupportedHeader (request) {
+  var allowUnregistered = request.ua.configuration.hackAllowUnregisteredOptionTags;
+  var optionTags = [];
+  var optionTagSet = {};
+
+  if (request.method === SIP.C.REGISTER) {
+    optionTags.push('path', 'gruu');
+  } else if (request.method === SIP.C.INVITE &&
+             (request.ua.contact.pub_gruu || request.ua.contact.temp_gruu)) {
+    optionTags.push('gruu');
+  }
+
+  if (request.ua.configuration.rel100 === SIP.C.supported.SUPPORTED) {
+    optionTags.push('100rel');
+  }
+  if (request.ua.configuration.replaces === SIP.C.supported.SUPPORTED) {
+    optionTags.push('replaces');
+  }
+
+  optionTags.push('outbound');
+
+  optionTags = optionTags.concat(request.ua.configuration.extraSupported);
+
+  optionTags = optionTags.filter(function(optionTag) {
+    var registered = SIP.C.OPTION_TAGS[optionTag];
+    var unique = !optionTagSet[optionTag];
+    optionTagSet[optionTag] = true;
+    return (registered || allowUnregistered) && unique;
+  });
+
+  return 'Supported: ' + optionTags.join(', ') + '\r\n';
+}
+
+/**
+ * @augments SIP
+ * @class Class for outgoing SIP request.
+ * @param {String} method request method
+ * @param {String} ruri request uri
+ * @param {SIP.UA} ua
+ * @param {Object} params parameters that will have priority over ua.configuration parameters:
+ * <br>
+ *  - cseq, call_id, from_tag, from_uri, from_displayName, to_uri, to_tag, route_set
+ * @param {Object} [headers] extra headers
+ * @param {String} [body]
+ */
+OutgoingRequest = function(method, ruri, ua, params, extraHeaders, body) {
+  var
+    to,
+    from,
+    call_id,
+    cseq,
+    to_uri,
+    from_uri;
+
+  params = params || {};
+
+  // Mandatory parameters check
+  if(!method || !ruri || !ua) {
+    return null;
+  }
+
+  this.logger = ua.getLogger('sip.sipmessage');
+  this.ua = ua;
+  this.headers = {};
+  this.method = method;
+  this.ruri = ruri;
+  this.body = body;
+  this.extraHeaders = (extraHeaders || []).slice();
+  this.statusCode = params.status_code;
+  this.reasonPhrase = params.reason_phrase;
+
+  // Fill the Common SIP Request Headers
+
+  // Route
+  if (params.route_set) {
+    this.setHeader('route', params.route_set);
+  } else if (ua.configuration.usePreloadedRoute){
+    this.setHeader('route', ua.transport.server.sip_uri);
+  }
+
+  // Via
+  // Empty Via header. Will be filled by the client transaction.
+  this.setHeader('via', '');
+
+  // Max-Forwards
+  this.setHeader('max-forwards', SIP.UA.C.MAX_FORWARDS);
+
+  // To
+  to_uri = params.to_uri || ruri;
+  to = (params.to_displayName || params.to_displayName === 0) ? '"' + params.to_displayName + '" ' : '';
+  to += '<' + (to_uri && to_uri.toRaw ? to_uri.toRaw() : to_uri) + '>';
+  to += params.to_tag ? ';tag=' + params.to_tag : '';
+  this.to = new SIP.NameAddrHeader.parse(to);
+  this.setHeader('to', to);
+
+  // From
+  from_uri = params.from_uri || ua.configuration.uri;
+  if (params.from_displayName || params.from_displayName === 0) {
+    from = '"' + params.from_displayName + '" ';
+  } else if (ua.configuration.displayName) {
+    from = '"' + ua.configuration.displayName + '" ';
+  } else {
+    from = '';
+  }
+  from += '<' + (from_uri && from_uri.toRaw ? from_uri.toRaw() : from_uri) + '>;tag=';
+  from += params.from_tag || SIP.Utils.newTag();
+  this.from = new SIP.NameAddrHeader.parse(from);
+  this.setHeader('from', from);
+
+  // Call-ID
+  call_id = params.call_id || (ua.configuration.sipjsId + SIP.Utils.createRandomToken(15));
+  this.call_id = call_id;
+  this.setHeader('call-id', call_id);
+
+  // CSeq
+  cseq = params.cseq || Math.floor(Math.random() * 10000);
+  this.cseq = cseq;
+  this.setHeader('cseq', cseq + ' ' + method);
+};
+
+OutgoingRequest.prototype = {
+  /**
+   * Replace the the given header by the given value.
+   * @param {String} name header name
+   * @param {String | Array} value header value
+   */
+  setHeader: function(name, value) {
+    this.headers[SIP.Utils.headerize(name)] = (value instanceof Array) ? value : [value];
+  },
+
+  /**
+   * Get the value of the given header name at the given position.
+   * @param {String} name header name
+   * @returns {String|undefined} Returns the specified header, undefined if header doesn't exist.
+   */
+  getHeader: function(name) {
+    var regexp, idx,
+      length = this.extraHeaders.length,
+      header = this.headers[SIP.Utils.headerize(name)];
+
+    if(header) {
+      if(header[0]) {
+        return header[0];
+      }
+    } else {
+      regexp = new RegExp('^\\s*' + name + '\\s*:','i');
+      for (idx = 0; idx < length; idx++) {
+        header = this.extraHeaders[idx];
+        if (regexp.test(header)) {
+          return header.substring(header.indexOf(':')+1).trim();
+        }
+      }
+    }
+
+    return;
+  },
+
+  /**
+   * Get the header/s of the given name.
+   * @param {String} name header name
+   * @returns {Array} Array with all the headers of the specified name.
+   */
+  getHeaders: function(name) {
+    var idx, length, regexp,
+      header = this.headers[SIP.Utils.headerize(name)],
+      result = [];
+
+    if(header) {
+      length = header.length;
+      for (idx = 0; idx < length; idx++) {
+        result.push(header[idx]);
+      }
+      return result;
+    } else {
+      length = this.extraHeaders.length;
+      regexp = new RegExp('^\\s*' + name + '\\s*:','i');
+      for (idx = 0; idx < length; idx++) {
+        header = this.extraHeaders[idx];
+        if (regexp.test(header)) {
+          result.push(header.substring(header.indexOf(':')+1).trim());
+        }
+      }
+      return result;
+    }
+  },
+
+  /**
+   * Verify the existence of the given header.
+   * @param {String} name header name
+   * @returns {boolean} true if header with given name exists, false otherwise
+   */
+  hasHeader: function(name) {
+    var regexp, idx,
+      length = this.extraHeaders.length;
+
+    if (this.headers[SIP.Utils.headerize(name)]) {
+      return true;
+    } else {
+      regexp = new RegExp('^\\s*' + name + '\\s*:','i');
+      for (idx = 0; idx < length; idx++) {
+        if (regexp.test(this.extraHeaders[idx])) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  },
+
+  toString: function() {
+    var msg = '', header, length, idx;
+
+    msg += this.method + ' ' + (this.ruri.toRaw ? this.ruri.toRaw() : this.ruri) + ' SIP/2.0\r\n';
+
+    for (header in this.headers) {
+      length = this.headers[header].length;
+      for (idx = 0; idx < length; idx++) {
+        msg += header + ': ' + this.headers[header][idx] + '\r\n';
+      }
+    }
+
+    length = this.extraHeaders.length;
+    for (idx = 0; idx < length; idx++) {
+      msg += this.extraHeaders[idx].trim() +'\r\n';
+    }
+
+    msg += getSupportedHeader(this);
+    msg += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n';
+
+    if (this.body) {
+      if (typeof this.body === 'string') {
+        length = SIP.Utils.str_utf8_length(this.body);
+        msg += 'Content-Length: ' + length + '\r\n\r\n';
+        msg += this.body;
+      } else {
+        if (this.body.body && this.body.contentType) {
+          length = SIP.Utils.str_utf8_length(this.body.body);
+          msg += 'Content-Type: ' + this.body.contentType + '\r\n';
+          msg += 'Content-Length: ' + length + '\r\n\r\n';
+          msg += this.body.body;
+        } else {
+          msg += 'Content-Length: ' + 0 + '\r\n\r\n';
+        }
+      }
+    } else {
+      msg += 'Content-Length: ' + 0 + '\r\n\r\n';
+    }
+
+    return msg;
+  }
+};
+
+/**
+ * @augments SIP
+ * @class Class for incoming SIP message.
+ */
+IncomingMessage = function(){
+  this.data = null;
+  this.headers = null;
+  this.method =  null;
+  this.via = null;
+  this.via_branch = null;
+  this.call_id = null;
+  this.cseq = null;
+  this.from = null;
+  this.from_tag = null;
+  this.to = null;
+  this.to_tag = null;
+  this.body = null;
+};
+
+IncomingMessage.prototype = {
+  /**
+  * Insert a header of the given name and value into the last position of the
+  * header array.
+  * @param {String} name header name
+  * @param {String} value header value
+  */
+  addHeader: function(name, value) {
+    var header = { raw: value };
+
+    name = SIP.Utils.headerize(name);
+
+    if(this.headers[name]) {
+      this.headers[name].push(header);
+    } else {
+      this.headers[name] = [header];
+    }
+  },
+
+  /**
+   * Get the value of the given header name at the given position.
+   * @param {String} name header name
+   * @returns {String|undefined} Returns the specified header, null if header doesn't exist.
+   */
+  getHeader: function(name) {
+    var header = this.headers[SIP.Utils.headerize(name)];
+
+    if(header) {
+      if(header[0]) {
+        return header[0].raw;
+      }
+    } else {
+      return;
+    }
+  },
+
+  /**
+   * Get the header/s of the given name.
+   * @param {String} name header name
+   * @returns {Array} Array with all the headers of the specified name.
+   */
+  getHeaders: function(name) {
+    var idx, length,
+      header = this.headers[SIP.Utils.headerize(name)],
+      result = [];
+
+    if(!header) {
+      return [];
+    }
+
+    length = header.length;
+    for (idx = 0; idx < length; idx++) {
+      result.push(header[idx].raw);
+    }
+
+    return result;
+  },
+
+  /**
+   * Verify the existence of the given header.
+   * @param {String} name header name
+   * @returns {boolean} true if header with given name exists, false otherwise
+   */
+  hasHeader: function(name) {
+    return(this.headers[SIP.Utils.headerize(name)]) ? true : false;
+  },
+
+  /**
+  * Parse the given header on the given index.
+  * @param {String} name header name
+  * @param {Number} [idx=0] header index
+  * @returns {Object|undefined} Parsed header object, undefined if the header is not present or in case of a parsing error.
+  */
+  parseHeader: function(name, idx) {
+    var header, value, parsed;
+
+    name = SIP.Utils.headerize(name);
+
+    idx = idx || 0;
+
+    if(!this.headers[name]) {
+      this.logger.log('header "' + name + '" not present');
+      return;
+    } else if(idx >= this.headers[name].length) {
+      this.logger.log('not so many "' + name + '" headers present');
+      return;
+    }
+
+    header = this.headers[name][idx];
+    value = header.raw;
+
+    if(header.parsed) {
+      return header.parsed;
+    }
+
+    //substitute '-' by '_' for grammar rule matching.
+    parsed = SIP.Grammar.parse(value, name.replace(/-/g, '_'));
+
+    if(parsed === -1) {
+      this.headers[name].splice(idx, 1); //delete from headers
+      this.logger.warn('error parsing "' + name + '" header field with value "' + value + '"');
+      return;
+    } else {
+      header.parsed = parsed;
+      return parsed;
+    }
+  },
+
+  /**
+   * Message Header attribute selector. Alias of parseHeader.
+   * @param {String} name header name
+   * @param {Number} [idx=0] header index
+   * @returns {Object|undefined} Parsed header object, undefined if the header is not present or in case of a parsing error.
+   *
+   * @example
+   * message.s('via',3).port
+   */
+  s: function(name, idx) {
+    return this.parseHeader(name, idx);
+  },
+
+  /**
+  * Replace the value of the given header by the value.
+  * @param {String} name header name
+  * @param {String} value header value
+  */
+  setHeader: function(name, value) {
+    var header = { raw: value };
+    this.headers[SIP.Utils.headerize(name)] = [header];
+  },
+
+  toString: function() {
+    return this.data;
+  }
+};
+
+/**
+ * @augments IncomingMessage
+ * @class Class for incoming SIP request.
+ */
+IncomingRequest = function(ua) {
+  this.logger = ua.getLogger('sip.sipmessage');
+  this.ua = ua;
+  this.headers = {};
+  this.ruri = null;
+  this.transport = null;
+  this.server_transaction = null;
+};
+IncomingRequest.prototype = new IncomingMessage();
+
+/**
+* Stateful reply.
+* @param {Number} code status code
+* @param {String} reason reason phrase
+* @param {Object} headers extra headers
+* @param {String} body body
+* @param {Function} [onSuccess] onSuccess callback
+* @param {Function} [onFailure] onFailure callback
+*/
+IncomingRequest.prototype.reply = function(code, reason, extraHeaders, body, onSuccess, onFailure) {
+  var rr, vias, length, idx, response,
+    to = this.getHeader('To'),
+    r = 0,
+    v = 0;
+
+  response = SIP.Utils.buildStatusLine(code, reason);
+  extraHeaders = (extraHeaders || []).slice();
+
+  if(this.method === SIP.C.INVITE && code > 100 && code <= 200) {
+    rr = this.getHeaders('record-route');
+    length = rr.length;
+
+    for(r; r < length; r++) {
+      response += 'Record-Route: ' + rr[r] + '\r\n';
+    }
+  }
+
+  vias = this.getHeaders('via');
+  length = vias.length;
+
+  for(v; v < length; v++) {
+    response += 'Via: ' + vias[v] + '\r\n';
+  }
+
+  if(!this.to_tag && code > 100) {
+    to += ';tag=' + SIP.Utils.newTag();
+  } else if(this.to_tag && !this.s('to').hasParam('tag')) {
+    to += ';tag=' + this.to_tag;
+  }
+
+  response += 'To: ' + to + '\r\n';
+  response += 'From: ' + this.getHeader('From') + '\r\n';
+  response += 'Call-ID: ' + this.call_id + '\r\n';
+  response += 'CSeq: ' + this.cseq + ' ' + this.method + '\r\n';
+
+  length = extraHeaders.length;
+  for (idx = 0; idx < length; idx++) {
+    response += extraHeaders[idx].trim() +'\r\n';
+  }
+
+  response += getSupportedHeader(this);
+  response += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n';
+
+  if (body) {
+    if (typeof body === 'string') {
+      length = SIP.Utils.str_utf8_length(body);
+      response += 'Content-Type: application/sdp\r\n';
+      response += 'Content-Length: ' + length + '\r\n\r\n';
+      response += body;
+    } else {
+      if (body.body && body.contentType) {
+        length = SIP.Utils.str_utf8_length(body.body);
+        response += 'Content-Type: ' + body.contentType + '\r\n';
+        response += 'Content-Length: ' + length + '\r\n\r\n';
+        response += body.body;
+      } else {
+        response += 'Content-Length: ' + 0 + '\r\n\r\n';
+      }
+    }
+  } else {
+    response += 'Content-Length: ' + 0 + '\r\n\r\n';
+  }
+
+  this.server_transaction.receiveResponse(code, response).then(onSuccess, onFailure);
+
+  return response;
+};
+
+/**
+* Stateless reply.
+* @param {Number} code status code
+* @param {String} reason reason phrase
+*/
+IncomingRequest.prototype.reply_sl = function(code, reason) {
+  var to, response,
+    v = 0,
+    vias = this.getHeaders('via'),
+    length = vias.length;
+
+  response = SIP.Utils.buildStatusLine(code, reason);
+
+  for(v; v < length; v++) {
+    response += 'Via: ' + vias[v] + '\r\n';
+  }
+
+  to = this.getHeader('To');
+
+  if(!this.to_tag && code > 100) {
+    to += ';tag=' + SIP.Utils.newTag();
+  } else if(this.to_tag && !this.s('to').hasParam('tag')) {
+    to += ';tag=' + this.to_tag;
+  }
+
+  response += 'To: ' + to + '\r\n';
+  response += 'From: ' + this.getHeader('From') + '\r\n';
+  response += 'Call-ID: ' + this.call_id + '\r\n';
+  response += 'CSeq: ' + this.cseq + ' ' + this.method + '\r\n';
+  response += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n';
+  response += 'Content-Length: ' + 0 + '\r\n\r\n';
+
+  this.transport.send(response);
+};
+
+
+/**
+ * @augments IncomingMessage
+ * @class Class for incoming SIP response.
+ */
+IncomingResponse = function(ua) {
+  this.logger = ua.getLogger('sip.sipmessage');
+  this.headers = {};
+  this.status_code = null;
+  this.reason_phrase = null;
+};
+IncomingResponse.prototype = new IncomingMessage();
+
+SIP.OutgoingRequest = OutgoingRequest;
+SIP.IncomingRequest = IncomingRequest;
+SIP.IncomingResponse = IncomingResponse;
+};
+
+},{}],21:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview Incoming SIP Message Sanity Check
+ */
+
+/**
+ * SIP message sanity check.
+ * @augments SIP
+ * @function
+ * @param {SIP.IncomingMessage} message
+ * @param {SIP.UA} ua
+ * @param {SIP.Transport} transport
+ * @returns {Boolean}
+ */
+module.exports = function (SIP) {
+var sanityCheck,
+ requests = [],
+ responses = [],
+ all = [];
+
+// Reply
+function reply(status_code, message, transport) {
+  var to,
+    response = SIP.Utils.buildStatusLine(status_code),
+    vias = message.getHeaders('via'),
+    length = vias.length,
+    idx = 0;
+
+  for(idx; idx < length; idx++) {
+    response += "Via: " + vias[idx] + "\r\n";
+  }
+
+  to = message.getHeader('To');
+
+  if(!message.to_tag) {
+    to += ';tag=' + SIP.Utils.newTag();
+  }
+
+  response += "To: " + to + "\r\n";
+  response += "From: " + message.getHeader('From') + "\r\n";
+  response += "Call-ID: " + message.call_id + "\r\n";
+  response += "CSeq: " + message.cseq + " " + message.method + "\r\n";
+  response += "\r\n";
+
+  transport.send(response);
+}
+
+/*
+ * Sanity Check for incoming Messages
+ *
+ * Requests:
+ *  - _rfc3261_8_2_2_1_ Receive a Request with a non supported URI scheme
+ *  - _rfc3261_16_3_4_ Receive a Request already sent by us
+ *   Does not look at via sent-by but at sipjsId, which is inserted as
+ *   a prefix in all initial requests generated by the ua
+ *  - _rfc3261_18_3_request_ Body Content-Length
+ *  - _rfc3261_8_2_2_2_ Merged Requests
+ *
+ * Responses:
+ *  - _rfc3261_8_1_3_3_ Multiple Via headers
+ *  - _rfc3261_18_1_2_ sent-by mismatch
+ *  - _rfc3261_18_3_response_ Body Content-Length
+ *
+ * All:
+ *  - Minimum headers in a SIP message
+ */
+
+// Sanity Check functions for requests
+function rfc3261_8_2_2_1(message, ua, transport) {
+  if(!message.ruri || message.ruri.scheme !== 'sip') {
+    reply(416, message, transport);
+    return false;
+  }
+}
+
+function rfc3261_16_3_4(message, ua, transport) {
+  if(!message.to_tag) {
+    if(message.call_id.substr(0, 5) === ua.configuration.sipjsId) {
+      reply(482, message, transport);
+      return false;
+    }
+  }
+}
+
+function rfc3261_18_3_request(message, ua, transport) {
+  var len = SIP.Utils.str_utf8_length(message.body),
+  contentLength = message.getHeader('content-length');
+
+  if(len < contentLength) {
+    reply(400, message, transport);
+    return false;
+  }
+}
+
+function rfc3261_8_2_2_2(message, ua, transport) {
+  var tr, idx,
+    fromTag = message.from_tag,
+    call_id = message.call_id,
+    cseq = message.cseq;
+
+  if(!message.to_tag) {
+    if(message.method === SIP.C.INVITE) {
+      tr = ua.transactions.ist[message.via_branch];
+      if(tr) {
+        return;
+      } else {
+        for(idx in ua.transactions.ist) {
+          tr = ua.transactions.ist[idx];
+          if(tr.request.from_tag === fromTag && tr.request.call_id === call_id && tr.request.cseq === cseq) {
+            reply(482, message, transport);
+            return false;
+          }
+        }
+      }
+    } else {
+      tr = ua.transactions.nist[message.via_branch];
+      if(tr) {
+        return;
+      } else {
+        for(idx in ua.transactions.nist) {
+          tr = ua.transactions.nist[idx];
+          if(tr.request.from_tag === fromTag && tr.request.call_id === call_id && tr.request.cseq === cseq) {
+            reply(482, message, transport);
+            return false;
+          }
+        }
+      }
+    }
+  }
+}
+
+// Sanity Check functions for responses
+function rfc3261_8_1_3_3(message, ua) {
+  if(message.getHeaders('via').length > 1) {
+    ua.getLogger('sip.sanitycheck').warn('More than one Via header field present in the response. Dropping the response');
+    return false;
+  }
+}
+
+function rfc3261_18_1_2(message, ua) {
+  var viaHost = ua.configuration.viaHost;
+  if(message.via.host !== viaHost || message.via.port !== undefined) {
+    ua.getLogger('sip.sanitycheck').warn('Via sent-by in the response does not match UA Via host value. Dropping the response');
+    return false;
+  }
+}
+
+function rfc3261_18_3_response(message, ua) {
+  var
+    len = SIP.Utils.str_utf8_length(message.body),
+    contentLength = message.getHeader('content-length');
+
+    if(len < contentLength) {
+      ua.getLogger('sip.sanitycheck').warn('Message body length is lower than the value in Content-Length header field. Dropping the response');
+      return false;
+    }
+}
+
+// Sanity Check functions for requests and responses
+function minimumHeaders(message, ua) {
+  var
+    mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'],
+    idx = mandatoryHeaders.length;
+
+  while(idx--) {
+    if(!message.hasHeader(mandatoryHeaders[idx])) {
+      ua.getLogger('sip.sanitycheck').warn('Missing mandatory header field : '+ mandatoryHeaders[idx] +'. Dropping the response');
+      return false;
+    }
+  }
+}
+
+requests.push(rfc3261_8_2_2_1);
+requests.push(rfc3261_16_3_4);
+requests.push(rfc3261_18_3_request);
+requests.push(rfc3261_8_2_2_2);
+
+responses.push(rfc3261_8_1_3_3);
+responses.push(rfc3261_18_1_2);
+responses.push(rfc3261_18_3_response);
+
+all.push(minimumHeaders);
+
+sanityCheck = function(message, ua, transport) {
+  var len, pass;
+
+  len = all.length;
+  while(len--) {
+    pass = all[len](message, ua, transport);
+    if(pass === false) {
+      return false;
+    }
+  }
+
+  if(message instanceof SIP.IncomingRequest) {
+    len = requests.length;
+    while(len--) {
+      pass = requests[len](message, ua, transport);
+      if(pass === false) {
+        return false;
+      }
+    }
+  }
+
+  else if(message instanceof SIP.IncomingResponse) {
+    len = responses.length;
+    while(len--) {
+      pass = responses[len](message, ua, transport);
+      if(pass === false) {
+        return false;
+      }
+    }
+  }
+
+  //Everything is OK
+  return true;
+};
+
+SIP.sanityCheck = sanityCheck;
+};
+
+},{}],22:[function(require,module,exports){
+"use strict";
+module.exports = function (SIP) {
+var ServerContext;
+
+ServerContext = function (ua, request) {
+  this.ua = ua;
+  this.logger = ua.getLogger('sip.servercontext');
+  this.request = request;
+  if (request.method === SIP.C.INVITE) {
+    this.transaction = new SIP.Transactions.InviteServerTransaction(request, ua);
+  } else {
+    this.transaction = new SIP.Transactions.NonInviteServerTransaction(request, ua);
+  }
+
+  if (request.body) {
+    this.body = request.body;
+  }
+  if (request.hasHeader('Content-Type')) {
+    this.contentType = request.getHeader('Content-Type');
+  }
+  this.method = request.method;
+
+  this.data = {};
+
+  this.localIdentity = request.to;
+  this.remoteIdentity = request.from;
+};
+
+ServerContext.prototype = Object.create(SIP.EventEmitter.prototype);
+
+ServerContext.prototype.progress = function (options) {
+  options = Object.create(options || Object.prototype);
+  options.statusCode || (options.statusCode = 180);
+  options.minCode = 100;
+  options.maxCode = 199;
+  options.events = ['progress'];
+  return this.reply(options);
+};
+
+ServerContext.prototype.accept = function (options) {
+  options = Object.create(options || Object.prototype);
+  options.statusCode || (options.statusCode = 200);
+  options.minCode = 200;
+  options.maxCode = 299;
+  options.events = ['accepted'];
+  return this.reply(options);
+};
+
+ServerContext.prototype.reject = function (options) {
+  options = Object.create(options || Object.prototype);
+  options.statusCode || (options.statusCode = 480);
+  options.minCode = 300;
+  options.maxCode = 699;
+  options.events = ['rejected', 'failed'];
+  return this.reply(options);
+};
+
+ServerContext.prototype.reply = function (options) {
+  options = options || {}; // This is okay, so long as we treat options as read-only in this method
+  var
+    statusCode = options.statusCode || 100,
+    minCode = options.minCode || 100,
+    maxCode = options.maxCode || 699,
+    reasonPhrase = SIP.Utils.getReasonPhrase(statusCode, options.reasonPhrase),
+    extraHeaders = options.extraHeaders || [],
+    body = options.body,
+    events = options.events || [],
+    response;
+
+  if (statusCode < minCode || statusCode > maxCode) {
+    throw new TypeError('Invalid statusCode: ' + statusCode);
+  }
+  response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
+  events.forEach(function (event) {
+    this.emit(event, response, reasonPhrase);
+  }, this);
+
+  return this;
+};
+
+ServerContext.prototype.onRequestTimeout = function () {
+  this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT);
+};
+
+ServerContext.prototype.onTransportError = function () {
+  this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR);
+};
+
+SIP.ServerContext = ServerContext;
+};
+
+},{}],23:[function(require,module,exports){
+"use strict";
+module.exports = function (SIP, environment) {
+
+var DTMF = require('./Session/DTMF')(SIP);
+
+var Session, InviteServerContext, InviteClientContext,
+ C = {
+    //Session states
+    STATUS_NULL:                        0,
+    STATUS_INVITE_SENT:                 1,
+    STATUS_1XX_RECEIVED:                2,
+    STATUS_INVITE_RECEIVED:             3,
+    STATUS_WAITING_FOR_ANSWER:          4,
+    STATUS_ANSWERED:                    5,
+    STATUS_WAITING_FOR_PRACK:           6,
+    STATUS_WAITING_FOR_ACK:             7,
+    STATUS_CANCELED:                    8,
+    STATUS_TERMINATED:                  9,
+    STATUS_ANSWERED_WAITING_FOR_PRACK: 10,
+    STATUS_EARLY_MEDIA:                11,
+    STATUS_CONFIRMED:                  12
+  };
+
+/*
+ * @param {function returning SIP.MediaHandler} [mediaHandlerFactory]
+ *        (See the documentation for the mediaHandlerFactory argument of the UA constructor.)
+ */
+Session = function (mediaHandlerFactory) {
+  this.status = C.STATUS_NULL;
+  this.dialog = null;
+  this.earlyDialogs = {};
+  this.mediaHandlerFactory = mediaHandlerFactory || SIP.WebRTC.MediaHandler.defaultFactory;
+  // this.mediaHandler gets set by ICC/ISC constructors
+  this.hasOffer = false;
+  this.hasAnswer = false;
+
+  // Session Timers
+  this.timers = {
+    ackTimer: null,
+    expiresTimer: null,
+    invite2xxTimer: null,
+    userNoAnswerTimer: null,
+    rel1xxTimer: null,
+    prackTimer: null
+  };
+
+  // Session info
+  this.startTime = null;
+  this.endTime = null;
+  this.tones = null;
+
+  // Mute/Hold state
+  this.local_hold = false;
+  this.remote_hold = false;
+
+  this.pending_actions = {
+    actions: [],
+
+    length: function() {
+      return this.actions.length;
+    },
+
+    isPending: function(name){
+      var
+      idx = 0,
+      length = this.actions.length;
+
+      for (idx; idx<length; idx++) {
+        if (this.actions[idx].name === name) {
+          return true;
+        }
+      }
+      return false;
+    },
+
+    shift: function() {
+      return this.actions.shift();
+    },
+
+    push: function(name) {
+      this.actions.push({
+        name: name
+      });
+    },
+
+    pop: function(name) {
+      var
+      idx = 0,
+      length = this.actions.length;
+
+      for (idx; idx<length; idx++) {
+        if (this.actions[idx].name === name) {
+          this.actions.splice(idx,1);
+          length --;
+          idx--;
+        }
+      }
+    }
+   };
+
+  this.early_sdp = null;
+  this.rel100 = SIP.C.supported.UNSUPPORTED;
+};
+
+Session.prototype = {
+  dtmf: function(tones, options) {
+    var tone, dtmfs = [],
+        self = this;
+
+    options = options || {};
+
+    if (tones === undefined) {
+      throw new TypeError('Not enough arguments');
+    }
+
+    // Check Session Status
+    if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_WAITING_FOR_ACK) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    // Check tones
+    if ((typeof tones !== 'string' && typeof tones !== 'number') || !tones.toString().match(/^[0-9A-D#*,]+$/i)) {
+      throw new TypeError('Invalid tones: '+ tones);
+    }
+
+    tones = tones.toString().split('');
+
+    while (tones.length > 0) { dtmfs.push(new DTMF(this, tones.shift(), options)); }
+
+    if (this.tones) {
+      // Tones are already queued, just add to the queue
+      this.tones =  this.tones.concat(dtmfs);
+      return this;
+    }
+
+    var sendDTMF = function () {
+      var dtmf, timeout;
+
+      if (self.status === C.STATUS_TERMINATED || !self.tones || self.tones.length === 0) {
+        // Stop sending DTMF
+        self.tones = null;
+        return this;
+      }
+
+      dtmf = self.tones.shift();
+
+      if (tone === ',') {
+        timeout = 2000;
+      } else {
+        dtmf.on('failed', function(){self.tones = null;});
+        dtmf.send(options);
+        timeout = dtmf.duration + dtmf.interToneGap;
+      }
+
+      // Set timeout for the next tone
+      SIP.Timers.setTimeout(sendDTMF, timeout);
+    };
+
+    this.tones = dtmfs;
+    sendDTMF();
+    return this;
+  },
+
+  bye: function(options) {
+    options = Object.create(options || Object.prototype);
+    var statusCode = options.statusCode;
+
+    // Check Session Status
+    if (this.status === C.STATUS_TERMINATED) {
+      this.logger.error('Error: Attempted to send BYE in a terminated session.');
+      return this;
+    }
+
+    this.logger.log('terminating Session');
+
+    if (statusCode && (statusCode < 200 || statusCode >= 700)) {
+      throw new TypeError('Invalid statusCode: '+ statusCode);
+    }
+
+    options.receiveResponse = function () {};
+
+    return this.
+      sendRequest(SIP.C.BYE, options).
+      terminated();
+  },
+
+  refer: function(target, options) {
+    options = options || {};
+    var extraHeaders = (options.extraHeaders || []).slice(),
+        withReplaces =
+          target instanceof SIP.InviteServerContext ||
+          target instanceof SIP.InviteClientContext,
+        originalTarget = target;
+
+    if (target === undefined) {
+      throw new TypeError('Not enough arguments');
+    }
+
+    // Check Session Status
+    if (this.status !== C.STATUS_CONFIRMED) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    // transform `target` so that it can be a Refer-To header value
+    if (withReplaces) {
+      //Attended Transfer
+      // B.transfer(C)
+      target = '"' + target.remoteIdentity.friendlyName + '" ' +
+        '<' + target.dialog.remote_target.toString() +
+        '?Replaces=' + target.dialog.id.call_id +
+        '%3Bto-tag%3D' + target.dialog.id.remote_tag +
+        '%3Bfrom-tag%3D' + target.dialog.id.local_tag + '>';
+    } else {
+      //Blind Transfer
+      // normalizeTarget allows instances of SIP.URI to pass through unaltered,
+      // so try to make one ahead of time
+      try {
+        target = SIP.Grammar.parse(target, 'Refer_To').uri || target;
+      } catch (e) {
+        this.logger.debug(".refer() cannot parse Refer_To from", target);
+        this.logger.debug("...falling through to normalizeTarget()");
+      }
+
+      // Check target validity
+      target = this.ua.normalizeTarget(target);
+      if (!target) {
+        throw new TypeError('Invalid target: ' + originalTarget);
+      }
+    }
+
+    extraHeaders.push('Contact: '+ this.contact);
+    extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString());
+    extraHeaders.push('Refer-To: '+ target);
+
+    // Send the request
+    this.sendRequest(SIP.C.REFER, {
+      extraHeaders: extraHeaders,
+      body: options.body,
+      receiveResponse: function (response) {
+        if ( ! /^2[0-9]{2}$/.test(response.status_code) ) {
+          return;
+        }
+        // hang up only if we transferred to a SIP address
+        if (withReplaces || (target.scheme && target.scheme.match("^sips?$"))) {
+          this.terminate();
+        }
+      }.bind(this)
+    });
+    return this;
+  },
+
+  followRefer: function followRefer (callback) {
+    return function referListener (callback, request) {
+      // open non-SIP URIs if possible and keep session open
+      var referTo = request.parseHeader('refer-to');
+      var target = referTo.uri;
+      if (!target.scheme.match("^sips?$")) {
+        var targetString = target.toString();
+        if (typeof environment.open === "function") {
+          environment.open(targetString);
+        } else {
+          this.logger.warn("referred to non-SIP URI but `open` isn't in the environment: " + targetString);
+        }
+        return;
+      }
+
+      var extraHeaders = [];
+
+      /* Copy the Replaces query into a Replaces header */
+      /* TODO - make sure we don't copy a poorly formatted header? */
+      var replaces = target.getHeader('Replaces');
+      if (replaces !== undefined) {
+        extraHeaders.push('Replaces: ' + decodeURIComponent(replaces));
+      }
+
+      // don't embed headers into Request-URI of INVITE
+      target.clearHeaders();
+
+      /*
+        Harmless race condition.  Both sides of REFER
+        may send a BYE, but in the end the dialogs are destroyed.
+      */
+      var getReferMedia = this.mediaHandler.getReferMedia;
+      var mediaHint = getReferMedia ? getReferMedia.call(this.mediaHandler) : this.mediaHint;
+
+      SIP.Hacks.Chrome.getsConfusedAboutGUM(this);
+
+      var referSession = this.ua.invite(target, {
+        media: mediaHint,
+        params: {
+          to_displayName: referTo.friendlyName
+        },
+        extraHeaders: extraHeaders
+      });
+
+      callback.call(this, request, referSession);
+
+      this.terminate();
+    }.bind(this, callback);
+  },
+
+  sendRequest: function(method,options) {
+    options = options || {};
+    var self = this;
+
+    var request = new SIP.OutgoingRequest(
+      method,
+      this.dialog.remote_target,
+      this.ua,
+      {
+        cseq: options.cseq || (this.dialog.local_seqnum += 1),
+        call_id: this.dialog.id.call_id,
+        from_uri: this.dialog.local_uri,
+        from_tag: this.dialog.id.local_tag,
+        to_uri: this.dialog.remote_uri,
+        to_tag: this.dialog.id.remote_tag,
+        route_set: this.dialog.route_set,
+        statusCode: options.statusCode,
+        reasonPhrase: options.reasonPhrase
+      },
+      options.extraHeaders || [],
+      options.body
+    );
+
+    new SIP.RequestSender({
+      request: request,
+      onRequestTimeout: function() {
+        self.onRequestTimeout();
+      },
+      onTransportError: function() {
+        self.onTransportError();
+      },
+      receiveResponse: options.receiveResponse || function(response) {
+        self.receiveNonInviteResponse(response);
+      }
+    }, this.ua).send();
+
+    // Emit the request event
+    this.emit(method.toLowerCase(), request);
+
+    return this;
+  },
+
+  close: function() {
+    var idx;
+
+    if(this.status === C.STATUS_TERMINATED) {
+      return this;
+    }
+
+    this.logger.log('closing INVITE session ' + this.id);
+
+    // 1st Step. Terminate media.
+    if (this.mediaHandler){
+      this.mediaHandler.close();
+    }
+
+    // 2nd Step. Terminate signaling.
+
+    // Clear session timers
+    for(idx in this.timers) {
+      SIP.Timers.clearTimeout(this.timers[idx]);
+    }
+
+    // Terminate dialogs
+
+    // Terminate confirmed dialog
+    if(this.dialog) {
+      this.dialog.terminate();
+      delete this.dialog;
+    }
+
+    // Terminate early dialogs
+    for(idx in this.earlyDialogs) {
+      this.earlyDialogs[idx].terminate();
+      delete this.earlyDialogs[idx];
+    }
+
+    this.status = C.STATUS_TERMINATED;
+
+    delete this.ua.sessions[this.id];
+    return this;
+  },
+
+  createDialog: function(message, type, early) {
+    var dialog, early_dialog,
+      local_tag = message[(type === 'UAS') ? 'to_tag' : 'from_tag'],
+      remote_tag = message[(type === 'UAS') ? 'from_tag' : 'to_tag'],
+      id = message.call_id + local_tag + remote_tag;
+
+    early_dialog = this.earlyDialogs[id];
+
+    // Early Dialog
+    if (early) {
+      if (early_dialog) {
+        return true;
+      } else {
+        early_dialog = new SIP.Dialog(this, message, type, SIP.Dialog.C.STATUS_EARLY);
+
+        // Dialog has been successfully created.
+        if(early_dialog.error) {
+          this.logger.error(early_dialog.error);
+          this.failed(message, SIP.C.causes.INTERNAL_ERROR);
+          return false;
+        } else {
+          this.earlyDialogs[id] = early_dialog;
+          return true;
+        }
+      }
+    }
+    // Confirmed Dialog
+    else {
+      // In case the dialog is in _early_ state, update it
+      if (early_dialog) {
+        early_dialog.update(message, type);
+        this.dialog = early_dialog;
+        delete this.earlyDialogs[id];
+        for (var dia in this.earlyDialogs) {
+          this.earlyDialogs[dia].terminate();
+          delete this.earlyDialogs[dia];
+        }
+        return true;
+      }
+
+      // Otherwise, create a _confirmed_ dialog
+      dialog = new SIP.Dialog(this, message, type);
+
+      if(dialog.error) {
+        this.logger.error(dialog.error);
+        this.failed(message, SIP.C.causes.INTERNAL_ERROR);
+        return false;
+      } else {
+        this.to_tag = message.to_tag;
+        this.dialog = dialog;
+        return true;
+      }
+    }
+  },
+
+  /**
+  * Check if Session is ready for a re-INVITE
+  *
+  * @returns {Boolean}
+  */
+  isReadyToReinvite: function() {
+    return this.mediaHandler.isReady() &&
+      !this.dialog.uac_pending_reply &&
+      !this.dialog.uas_pending_reply;
+  },
+
+  /**
+   * Mute
+   */
+  mute: function(options) {
+    var ret = this.mediaHandler.mute(options);
+    if (ret) {
+      this.onmute(ret);
+    }
+  },
+
+  /**
+   * Unmute
+   */
+  unmute: function(options) {
+    var ret = this.mediaHandler.unmute(options);
+    if (ret) {
+      this.onunmute(ret);
+    }
+  },
+
+  /**
+   * Hold
+   */
+  hold: function(options) {
+
+    if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    this.mediaHandler.hold();
+
+    // Check if RTCSession is ready to send a reINVITE
+    if (!this.isReadyToReinvite()) {
+      /* If there is a pending 'unhold' action, cancel it and don't queue this one
+       * Else, if there isn't any 'hold' action, add this one to the queue
+       * Else, if there is already a 'hold' action, skip
+       */
+      if (this.pending_actions.isPending('unhold')) {
+        this.pending_actions.pop('unhold');
+      } else if (!this.pending_actions.isPending('hold')) {
+        this.pending_actions.push('hold');
+      }
+      return;
+    } else if (this.local_hold === true) {
+        return;
+    }
+
+    this.onhold('local');
+
+    this.sendReinvite(options);
+  },
+
+  /**
+   * Unhold
+   */
+  unhold: function(options) {
+
+    if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    this.mediaHandler.unhold();
+
+    if (!this.isReadyToReinvite()) {
+      /* If there is a pending 'hold' action, cancel it and don't queue this one
+       * Else, if there isn't any 'unhold' action, add this one to the queue
+       * Else, if there is already a 'unhold' action, skip
+       */
+      if (this.pending_actions.isPending('hold')) {
+        this.pending_actions.pop('hold');
+      } else if (!this.pending_actions.isPending('unhold')) {
+        this.pending_actions.push('unhold');
+      }
+      return;
+    } else if (this.local_hold === false) {
+      return;
+    }
+
+    this.onunhold('local');
+
+    this.sendReinvite(options);
+  },
+
+  /**
+   * isOnHold
+   */
+  isOnHold: function() {
+    return {
+      local: this.local_hold,
+      remote: this.remote_hold
+    };
+  },
+
+  /**
+   * In dialog INVITE Reception
+   * @private
+   */
+  receiveReinvite: function(request) {
+    var self = this;
+
+    if (!this.mediaHandler.hasDescription(request)) {
+      this.logger.warn('invalid Content-Type');
+      request.reply(415);
+      return;
+    }
+
+    this.mediaHandler.setDescription(request)
+    .then(this.mediaHandler.getDescription.bind(this.mediaHandler, this.mediaHint))
+    .then(function(description) {
+      var extraHeaders = ['Contact: ' + self.contact];
+      request.reply(200, null, extraHeaders, description,
+        function() {
+          self.status = C.STATUS_WAITING_FOR_ACK;
+          self.setInvite2xxTimer(request, description);
+          self.setACKTimer();
+
+          if (self.remote_hold && !self.mediaHandler.remote_hold) {
+            self.onunhold('remote');
+          } else if (!self.remote_hold && self.mediaHandler.remote_hold) {
+            self.onhold('remote');
+          }
+        });
+    })
+    .catch(function onFailure (e) {
+      var statusCode;
+      if (e instanceof SIP.Exceptions.GetDescriptionError) {
+        statusCode = 500;
+      } else {
+        self.logger.error(e);
+        statusCode = 488;
+      }
+      request.reply(statusCode);
+    });
+  },
+
+  sendReinvite: function(options) {
+    options = options || {};
+
+    var
+      self = this,
+       extraHeaders = (options.extraHeaders || []).slice(),
+       eventHandlers = options.eventHandlers || {},
+       succeeded;
+
+    if (eventHandlers.succeeded) {
+      succeeded = eventHandlers.succeeded;
+    }
+    this.reinviteSucceeded = function(){
+      SIP.Timers.clearTimeout(self.timers.ackTimer);
+      SIP.Timers.clearTimeout(self.timers.invite2xxTimer);
+      self.status = C.STATUS_CONFIRMED;
+      succeeded && succeeded.apply(this, arguments);
+    };
+    if (eventHandlers.failed) {
+      this.reinviteFailed = eventHandlers.failed;
+    } else {
+      this.reinviteFailed = function(){};
+    }
+
+    extraHeaders.push('Contact: ' + this.contact);
+    extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString());
+
+    this.receiveResponse = this.receiveReinviteResponse;
+    //REVISIT
+    this.mediaHandler.getDescription(self.mediaHint)
+    .then(
+      function(description){
+        self.dialog.sendRequest(self, SIP.C.INVITE, {
+          extraHeaders: extraHeaders,
+          body: description
+        });
+      },
+      function() {
+        if (self.isReadyToReinvite()) {
+          self.onReadyToReinvite();
+        }
+        self.reinviteFailed();
+      }
+    );
+  },
+
+  receiveRequest: function (request) {
+    switch (request.method) {
+      case SIP.C.BYE:
+        request.reply(200);
+        if(this.status === C.STATUS_CONFIRMED) {
+          this.emit('bye', request);
+          this.terminated(request, SIP.C.causes.BYE);
+        }
+        break;
+      case SIP.C.INVITE:
+        if(this.status === C.STATUS_CONFIRMED) {
+          this.logger.log('re-INVITE received');
+          this.receiveReinvite(request);
+        }
+        break;
+      case SIP.C.INFO:
+        if(this.status === C.STATUS_CONFIRMED || this.status === C.STATUS_WAITING_FOR_ACK) {
+          if (this.onInfo) {
+            return this.onInfo(request);
+          }
+
+          var body, tone, duration,
+              contentType = request.getHeader('content-type'),
+              reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/,
+              reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;
+
+          if (contentType) {
+            if (contentType.match(/^application\/dtmf-relay/i)) {
+              if (request.body) {
+                body = request.body.split('\r\n', 2);
+                if (body.length === 2) {
+                  if (reg_tone.test(body[0])) {
+                    tone = body[0].replace(reg_tone,"$2");
+                  }
+                  if (reg_duration.test(body[1])) {
+                    duration = parseInt(body[1].replace(reg_duration,"$2"), 10);
+                  }
+                }
+              }
+
+              new DTMF(this, tone, {duration: duration}).init_incoming(request);
+            } else {
+              request.reply(415, null, ["Accept: application/dtmf-relay"]);
+            }
+          }
+        }
+        break;
+      case SIP.C.REFER:
+        if(this.status ===  C.STATUS_CONFIRMED) {
+          this.logger.log('REFER received');
+          var hasReferListener = this.listeners('refer').length,
+              notifyBody;
+
+          if (hasReferListener) {
+            request.reply(202, 'Accepted');
+            notifyBody = 'SIP/2.0 100 Trying';
+
+            this.sendRequest(SIP.C.NOTIFY, {
+              extraHeaders:[
+                'Event: refer',
+                'Subscription-State: terminated',
+                'Content-Type: message/sipfrag'
+              ],
+              body: notifyBody,
+              receiveResponse: function() {}
+            });
+
+            this.emit('refer', request);
+          } else {
+            // RFC 3515.2.4.2: 'the UA MAY decline the request.'
+            request.reply(603, 'Declined');
+          }
+        }
+        break;
+      case SIP.C.NOTIFY:
+        request.reply(200, 'OK');
+        this.emit('notify', request);
+        break;
+    }
+  },
+
+  /**
+   * Reception of Response for in-dialog INVITE
+   * @private
+   */
+  receiveReinviteResponse: function(response) {
+    var self = this;
+
+    if (this.status === C.STATUS_TERMINATED) {
+      return;
+    }
+
+    switch(true) {
+      case /^1[0-9]{2}$/.test(response.status_code):
+        break;
+      case /^2[0-9]{2}$/.test(response.status_code):
+        this.status = C.STATUS_CONFIRMED;
+
+        this.sendRequest(SIP.C.ACK,{cseq:response.cseq});
+
+        if (!this.mediaHandler.hasDescription(response)) {
+          this.reinviteFailed();
+          break;
+        }
+
+        //REVISIT
+        this.mediaHandler.setDescription(response)
+        .then(
+          function onSuccess () {
+            self.reinviteSucceeded();
+          },
+          function onFailure () {
+            self.reinviteFailed();
+          }
+        );
+        break;
+      default:
+        this.reinviteFailed();
+    }
+  },
+
+  acceptAndTerminate: function(response, status_code, reason_phrase) {
+    var extraHeaders = [];
+
+    if (status_code) {
+      extraHeaders.push('Reason: ' + SIP.Utils.getReasonHeaderValue(status_code, reason_phrase));
+    }
+
+    // An error on dialog creation will fire 'failed' event
+    if (this.dialog || this.createDialog(response, 'UAC')) {
+      this.sendRequest(SIP.C.ACK,{cseq: response.cseq});
+      this.sendRequest(SIP.C.BYE, {
+        extraHeaders: extraHeaders
+      });
+    }
+
+    return this;
+  },
+
+  /**
+   * RFC3261 13.3.1.4
+   * Response retransmissions cannot be accomplished by transaction layer
+   *  since it is destroyed when receiving the first 2xx answer
+   */
+  setInvite2xxTimer: function(request, description) {
+    var self = this,
+        timeout = SIP.Timers.T1;
+
+    this.timers.invite2xxTimer = SIP.Timers.setTimeout(function invite2xxRetransmission() {
+      if (self.status !== C.STATUS_WAITING_FOR_ACK) {
+        return;
+      }
+
+      self.logger.log('no ACK received, attempting to retransmit OK');
+
+      var extraHeaders = ['Contact: ' + self.contact];
+
+      request.reply(200, null, extraHeaders, description);
+
+      timeout = Math.min(timeout * 2, SIP.Timers.T2);
+
+      self.timers.invite2xxTimer = SIP.Timers.setTimeout(invite2xxRetransmission, timeout);
+    }, timeout);
+  },
+
+  /**
+   * RFC3261 14.2
+   * If a UAS generates a 2xx response and never receives an ACK,
+   *  it SHOULD generate a BYE to terminate the dialog.
+   */
+  setACKTimer: function() {
+    var self = this;
+
+    this.timers.ackTimer = SIP.Timers.setTimeout(function() {
+      if(self.status === C.STATUS_WAITING_FOR_ACK) {
+        self.logger.log('no ACK received for an extended period of time, terminating the call');
+        SIP.Timers.clearTimeout(self.timers.invite2xxTimer);
+        self.sendRequest(SIP.C.BYE);
+        self.terminated(null, SIP.C.causes.NO_ACK);
+      }
+    }, SIP.Timers.TIMER_H);
+  },
+
+  /*
+   * @private
+   */
+  onReadyToReinvite: function() {
+    var action = this.pending_actions.shift();
+
+    if (!action || !this[action.name]) {
+      return;
+    }
+
+    this[action.name]();
+  },
+
+  onTransportError: function() {
+    if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.CONNECTION_ERROR);
+    }
+  },
+
+  onRequestTimeout: function() {
+    if (this.status === C.STATUS_CONFIRMED) {
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    } else if (this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.REQUEST_TIMEOUT);
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    }
+  },
+
+  onDialogError: function(response) {
+    if (this.status === C.STATUS_CONFIRMED) {
+      this.terminated(response, SIP.C.causes.DIALOG_ERROR);
+    } else if (this.status !== C.STATUS_TERMINATED) {
+      this.failed(response, SIP.C.causes.DIALOG_ERROR);
+      this.terminated(response, SIP.C.causes.DIALOG_ERROR);
+    }
+  },
+
+  /**
+   * @private
+   */
+  onhold: function(originator) {
+    this[originator === 'local' ? 'local_hold' : 'remote_hold'] = true;
+    this.emit('hold', { originator: originator });
+  },
+
+  /**
+   * @private
+   */
+  onunhold: function(originator) {
+    this[originator === 'local' ? 'local_hold' : 'remote_hold'] = false;
+    this.emit('unhold', { originator: originator });
+  },
+
+  /*
+   * @private
+   */
+  onmute: function(options) {
+    this.emit('muted', {
+      audio: options.audio,
+      video: options.video
+    });
+  },
+
+  /*
+   * @private
+   */
+  onunmute: function(options) {
+    this.emit('unmuted', {
+      audio: options.audio,
+      video: options.video
+    });
+  },
+
+  failed: function(response, cause) {
+    if (this.status === C.STATUS_TERMINATED) {
+      return this;
+    }
+    this.emit('failed', response || null, cause || null);
+    return this;
+  },
+
+  rejected: function(response, cause) {
+    this.emit('rejected',
+      response || null,
+      cause || null
+    );
+    return this;
+  },
+
+  canceled: function() {
+    this.emit('cancel');
+    return this;
+  },
+
+  accepted: function(response, cause) {
+    cause = SIP.Utils.getReasonPhrase(response && response.status_code, cause);
+
+    this.startTime = new Date();
+
+    if (this.replacee) {
+      this.replacee.emit('replaced', this);
+      this.replacee.terminate();
+    }
+    this.emit('accepted', response, cause);
+    return this;
+  },
+
+  terminated: function(message, cause) {
+    if (this.status === C.STATUS_TERMINATED) {
+      return this;
+    }
+
+    this.endTime = new Date();
+
+    this.close();
+    this.emit('terminated',
+      message || null,
+      cause || null
+    );
+    return this;
+  },
+
+  connecting: function(request) {
+    this.emit('connecting', { request: request });
+    return this;
+  }
+};
+
+Session.desugar = function desugar(options) {
+  if (environment.HTMLMediaElement && options instanceof environment.HTMLMediaElement) {
+    options = {
+      media: {
+        constraints: {
+          audio: true,
+          video: options.tagName === 'VIDEO'
+        },
+        render: {
+          remote: options
+        }
+      }
+    };
+  }
+  return options || {};
+};
+
+
+Session.C = C;
+SIP.Session = Session;
+
+
+InviteServerContext = function(ua, request) {
+  var expires,
+    self = this,
+    contentType = request.getHeader('Content-Type'),
+    contentDisp = request.parseHeader('Content-Disposition');
+
+  SIP.Utils.augment(this, SIP.ServerContext, [ua, request]);
+  SIP.Utils.augment(this, SIP.Session, [ua.configuration.mediaHandlerFactory]);
+
+  //Initialize Media Session
+  this.mediaHandler = this.mediaHandlerFactory(this, {
+    RTCConstraints: {"optional": [{'DtlsSrtpKeyAgreement': 'true'}]}
+  });
+
+  // Check body and content type
+  if ((!contentDisp && !this.mediaHandler.hasDescription(request)) || (contentDisp && contentDisp.type === 'render')) {
+    this.renderbody = request.body;
+    this.rendertype = contentType;
+  } else if (!this.mediaHandler.hasDescription(request) && (contentDisp && contentDisp.type === 'session')) {
+    request.reply(415);
+    //TODO: instead of 415, pass off to the media handler, who can then decide if we can use it
+    return;
+  }
+
+  this.status = C.STATUS_INVITE_RECEIVED;
+  this.from_tag = request.from_tag;
+  this.id = request.call_id + this.from_tag;
+  this.request = request;
+  this.contact = this.ua.contact.toString();
+
+  this.receiveNonInviteResponse = function () {}; // intentional no-op
+
+  this.logger = ua.getLogger('sip.inviteservercontext', this.id);
+
+  //Save the session into the ua sessions collection.
+  this.ua.sessions[this.id] = this;
+
+  //Get the Expires header value if exists
+  if(request.hasHeader('expires')) {
+    expires = request.getHeader('expires') * 1000;
+  }
+
+  //Set 100rel if necessary
+  function set100rel(h,c) {
+    if (request.hasHeader(h) && request.getHeader(h).toLowerCase().indexOf('100rel') >= 0) {
+      self.rel100 = c;
+    }
+  }
+  set100rel('require', SIP.C.supported.REQUIRED);
+  set100rel('supported', SIP.C.supported.SUPPORTED);
+
+  /* Set the to_tag before
+   * replying a response code that will create a dialog.
+   */
+  request.to_tag = SIP.Utils.newTag();
+
+  // An error on dialog creation will fire 'failed' event
+  if(!this.createDialog(request, 'UAS', true)) {
+    request.reply(500, 'Missing Contact header field');
+    return;
+  }
+
+  if (this.mediaHandler && this.mediaHandler.getRemoteStreams) {
+    this.getRemoteStreams = this.mediaHandler.getRemoteStreams.bind(this.mediaHandler);
+    this.getLocalStreams = this.mediaHandler.getLocalStreams.bind(this.mediaHandler);
+  }
+
+  function fireNewSession() {
+    var options = {extraHeaders: ['Contact: ' + self.contact]};
+
+    if (self.rel100 !== SIP.C.supported.REQUIRED) {
+      self.progress(options);
+    }
+    self.status = C.STATUS_WAITING_FOR_ANSWER;
+
+    // Set userNoAnswerTimer
+    self.timers.userNoAnswerTimer = SIP.Timers.setTimeout(function() {
+      request.reply(408);
+      self.failed(request, SIP.C.causes.NO_ANSWER);
+      self.terminated(request, SIP.C.causes.NO_ANSWER);
+    }, self.ua.configuration.noAnswerTimeout);
+
+    /* Set expiresTimer
+     * RFC3261 13.3.1
+     */
+    if (expires) {
+      self.timers.expiresTimer = SIP.Timers.setTimeout(function() {
+        if(self.status === C.STATUS_WAITING_FOR_ANSWER) {
+          request.reply(487);
+          self.failed(request, SIP.C.causes.EXPIRES);
+          self.terminated(request, SIP.C.causes.EXPIRES);
+        }
+      }, expires);
+    }
+
+    self.emit('invite',request);
+  }
+
+  if (!this.mediaHandler.hasDescription(request) || this.renderbody) {
+    SIP.Timers.setTimeout(fireNewSession, 0);
+  } else {
+    this.hasOffer = true;
+    this.mediaHandler.setDescription(request)
+    .then(
+      fireNewSession,
+      function onFailure (e) {
+        self.logger.warn('invalid description');
+        self.logger.warn(e);
+        request.reply(488);
+      }
+    );
+  }
+};
+
+InviteServerContext.prototype = {
+  reject: function(options) {
+    // Check Session Status
+    if (this.status === C.STATUS_TERMINATED) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    this.logger.log('rejecting RTCSession');
+
+    SIP.ServerContext.prototype.reject.call(this, options);
+    return this.terminated();
+  },
+
+  terminate: function(options) {
+    options = options || {};
+
+    var
+    extraHeaders = (options.extraHeaders || []).slice(),
+    body = options.body,
+    dialog,
+    self = this;
+
+    if (this.status === C.STATUS_WAITING_FOR_ACK &&
+       this.request.server_transaction.state !== SIP.Transactions.C.STATUS_TERMINATED) {
+      dialog = this.dialog;
+
+      this.receiveRequest = function(request) {
+        if (request.method === SIP.C.ACK) {
+          this.sendRequest(SIP.C.BYE, {
+            extraHeaders: extraHeaders,
+            body: body
+          });
+          dialog.terminate();
+        }
+      };
+
+      this.request.server_transaction.on('stateChanged', function(){
+        if (this.state === SIP.Transactions.C.STATUS_TERMINATED && this.dialog) {
+          this.request = new SIP.OutgoingRequest(
+            SIP.C.BYE,
+            this.dialog.remote_target,
+            this.ua,
+            {
+              'cseq': this.dialog.local_seqnum+=1,
+              'call_id': this.dialog.id.call_id,
+              'from_uri': this.dialog.local_uri,
+              'from_tag': this.dialog.id.local_tag,
+              'to_uri': this.dialog.remote_uri,
+              'to_tag': this.dialog.id.remote_tag,
+              'route_set': this.dialog.route_set
+            },
+            extraHeaders,
+            body
+          );
+
+          new SIP.RequestSender(
+            {
+              request: this.request,
+              onRequestTimeout: function() {
+                self.onRequestTimeout();
+              },
+              onTransportError: function() {
+                self.onTransportError();
+              },
+              receiveResponse: function() {
+                return;
+              }
+            },
+            this.ua
+          ).send();
+          dialog.terminate();
+        }
+      });
+
+      this.emit('bye', this.request);
+      this.terminated();
+
+      // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-)
+      this.dialog = dialog;
+
+      // Restore the dialog into 'ua' so the ACK can reach 'this' session
+      this.ua.dialogs[dialog.id.toString()] = dialog;
+
+    } else if (this.status === C.STATUS_CONFIRMED) {
+      this.bye(options);
+    } else {
+      this.reject(options);
+    }
+
+    return this;
+  },
+
+  /*
+   * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint
+   */
+  progress: function (options) {
+    options = options || {};
+    var
+      statusCode = options.statusCode || 180,
+      reasonPhrase = options.reasonPhrase,
+      extraHeaders = (options.extraHeaders || []).slice(),
+      iceServers,
+      stunServers = options.stunServers || null,
+      turnServers = options.turnServers || null,
+      body = options.body,
+      response;
+
+    if (statusCode < 100 || statusCode > 199) {
+      throw new TypeError('Invalid statusCode: ' + statusCode);
+    }
+
+    if (this.isCanceled || this.status === C.STATUS_TERMINATED) {
+      return this;
+    }
+
+    if (stunServers || turnServers) {
+      if (stunServers) {
+        iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers);
+        if (!iceServers) {
+          throw new TypeError('Invalid stunServers: '+ stunServers);
+        } else {
+          this.stunServers = iceServers;
+        }
+      }
+
+      if (turnServers) {
+        iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers);
+        if (!iceServers) {
+          throw new TypeError('Invalid turnServers: '+ turnServers);
+        } else {
+          this.turnServers = iceServers;
+        }
+      }
+
+      this.mediaHandler.updateIceServers({
+        stunServers: this.stunServers,
+        turnServers: this.turnServers
+      });
+    }
+
+    function do100rel() {
+      /* jshint validthis: true */
+      statusCode = options.statusCode || 183;
+
+      // Set status and add extra headers
+      this.status = C.STATUS_WAITING_FOR_PRACK;
+      extraHeaders.push('Contact: '+ this.contact);
+      extraHeaders.push('Require: 100rel');
+      extraHeaders.push('RSeq: ' + Math.floor(Math.random() * 10000));
+
+      // Save media hint for later (referred sessions)
+      this.mediaHint = options.media;
+
+      // Get the session description to add to preaccept with
+      this.mediaHandler.getDescription(options.media)
+      .then(
+        function onSuccess (description) {
+          if (this.isCanceled || this.status === C.STATUS_TERMINATED) {
+            return;
+          }
+
+          this.early_sdp = description.body;
+          this[this.hasOffer ? 'hasAnswer' : 'hasOffer'] = true;
+
+          // Retransmit until we get a response or we time out (see prackTimer below)
+          var timeout = SIP.Timers.T1;
+          this.timers.rel1xxTimer = SIP.Timers.setTimeout(function rel1xxRetransmission() {
+            this.request.reply(statusCode, null, extraHeaders, description);
+            timeout *= 2;
+            this.timers.rel1xxTimer = SIP.Timers.setTimeout(rel1xxRetransmission.bind(this), timeout);
+          }.bind(this), timeout);
+
+          // Timeout and reject INVITE if no response
+          this.timers.prackTimer = SIP.Timers.setTimeout(function () {
+            if (this.status !== C.STATUS_WAITING_FOR_PRACK) {
+              return;
+            }
+
+            this.logger.log('no PRACK received, rejecting the call');
+            SIP.Timers.clearTimeout(this.timers.rel1xxTimer);
+            this.request.reply(504);
+            this.terminated(null, SIP.C.causes.NO_PRACK);
+          }.bind(this), SIP.Timers.T1 * 64);
+
+          // Send the initial response
+          response = this.request.reply(statusCode, reasonPhrase, extraHeaders, description);
+          this.emit('progress', response, reasonPhrase);
+        }.bind(this),
+
+        function onFailure () {
+          this.request.reply(480);
+          this.failed(null, SIP.C.causes.WEBRTC_ERROR);
+          this.terminated(null, SIP.C.causes.WEBRTC_ERROR);
+        }.bind(this)
+      );
+    } // end do100rel
+
+    function normalReply() {
+      /* jshint validthis:true */
+      response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
+      this.emit('progress', response, reasonPhrase);
+    }
+
+    if (options.statusCode !== 100 &&
+        (this.rel100 === SIP.C.supported.REQUIRED ||
+         (this.rel100 === SIP.C.supported.SUPPORTED && options.rel100) ||
+         (this.rel100 === SIP.C.supported.SUPPORTED && (this.ua.configuration.rel100 === SIP.C.supported.REQUIRED)))) {
+      do100rel.apply(this);
+    } else {
+      normalReply.apply(this);
+    }
+    return this;
+  },
+
+  /*
+   * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint
+   */
+  accept: function(options) {
+    options = Object.create(Session.desugar(options));
+    SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media);
+    this.mediaHint = options.media;
+
+    this.onInfo = options.onInfo;
+
+    // commented out now-unused hold-related variables for jshint. See below. JMF 2014-1-21
+    var
+      //idx, length, hasAudio, hasVideo,
+      self = this,
+      request = this.request,
+      extraHeaders = (options.extraHeaders || []).slice(),
+    //mediaStream = options.mediaStream || null,
+      iceServers,
+      stunServers = options.stunServers || null,
+      turnServers = options.turnServers || null,
+      descriptionCreationSucceeded = function(description) {
+        var
+          response,
+          // run for reply success callback
+          replySucceeded = function() {
+            self.status = C.STATUS_WAITING_FOR_ACK;
+
+            self.setInvite2xxTimer(request, description);
+            self.setACKTimer();
+          },
+
+          // run for reply failure callback
+          replyFailed = function() {
+            self.failed(null, SIP.C.causes.CONNECTION_ERROR);
+            self.terminated(null, SIP.C.causes.CONNECTION_ERROR);
+          };
+
+        // Chrome might call onaddstream before accept() is called, which means
+        // mediaHandler.render() was called without a renderHint, so we need to
+        // re-render now that mediaHint.render has been set.
+        //
+        // Chrome seems to be in the right regarding this, see
+        // http://dev.w3.org/2011/webrtc/editor/webrtc.html#widl-RTCPeerConnection-onaddstream
+        self.mediaHandler.render();
+
+        extraHeaders.push('Contact: ' + self.contact);
+        extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString());
+
+        if(!self.hasOffer) {
+          self.hasOffer = true;
+        } else {
+          self.hasAnswer = true;
+        }
+        response = request.reply(200, null, extraHeaders,
+                      description,
+                      replySucceeded,
+                      replyFailed
+                     );
+        if (self.status !== C.STATUS_TERMINATED) { // Didn't fail
+          self.accepted(response, SIP.Utils.getReasonPhrase(200));
+        }
+      },
+
+      descriptionCreationFailed = function() {
+        if (self.status === C.STATUS_TERMINATED) {
+          return;
+        }
+        // TODO - fail out on error
+        self.request.reply(480);
+        //self.failed(response, SIP.C.causes.USER_DENIED_MEDIA_ACCESS);
+        self.failed(null, SIP.C.causes.WEBRTC_ERROR);
+        self.terminated(null, SIP.C.causes.WEBRTC_ERROR);
+      };
+
+    // Check Session Status
+    if (this.status === C.STATUS_WAITING_FOR_PRACK) {
+      this.status = C.STATUS_ANSWERED_WAITING_FOR_PRACK;
+      return this;
+    } else if (this.status === C.STATUS_WAITING_FOR_ANSWER) {
+      this.status = C.STATUS_ANSWERED;
+    } else if (this.status !== C.STATUS_EARLY_MEDIA) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    if ((stunServers || turnServers) &&
+        (this.status !== C.STATUS_EARLY_MEDIA && this.status !== C.STATUS_ANSWERED_WAITING_FOR_PRACK)) {
+      if (stunServers) {
+        iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers);
+        if (!iceServers) {
+          throw new TypeError('Invalid stunServers: '+ stunServers);
+        } else {
+          this.stunServers = iceServers;
+        }
+      }
+
+      if (turnServers) {
+        iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers);
+        if (!iceServers) {
+          throw new TypeError('Invalid turnServers: '+ turnServers);
+        } else {
+          this.turnServers = iceServers;
+        }
+      }
+
+      this.mediaHandler.updateIceServers({
+        stunServers: this.stunServers,
+        turnServers: this.turnServers
+      });
+    }
+
+    // An error on dialog creation will fire 'failed' event
+    if(!this.createDialog(request, 'UAS')) {
+      request.reply(500, 'Missing Contact header field');
+      return this;
+    }
+
+    SIP.Timers.clearTimeout(this.timers.userNoAnswerTimer);
+
+    // this hold-related code breaks FF accepting new calls - JMF 2014-1-21
+    /*
+    length = this.getRemoteStreams().length;
+
+    for (idx = 0; idx < length; idx++) {
+      if (this.mediaHandler.getRemoteStreams()[idx].getVideoTracks().length > 0) {
+        hasVideo = true;
+      }
+      if (this.mediaHandler.getRemoteStreams()[idx].getAudioTracks().length > 0) {
+        hasAudio = true;
+      }
+    }
+
+    if (!hasAudio && this.mediaConstraints.audio === true) {
+      this.mediaConstraints.audio = false;
+      if (mediaStream) {
+        length = mediaStream.getAudioTracks().length;
+        for (idx = 0; idx < length; idx++) {
+          mediaStream.removeTrack(mediaStream.getAudioTracks()[idx]);
+        }
+      }
+    }
+
+    if (!hasVideo && this.mediaConstraints.video === true) {
+      this.mediaConstraints.video = false;
+      if (mediaStream) {
+        length = mediaStream.getVideoTracks().length;
+        for (idx = 0; idx < length; idx++) {
+          mediaStream.removeTrack(mediaStream.getVideoTracks()[idx]);
+        }
+      }
+    }
+    */
+
+    if (this.status === C.STATUS_EARLY_MEDIA) {
+      descriptionCreationSucceeded({});
+    } else {
+      this.mediaHandler.getDescription(self.mediaHint)
+      .then(
+        descriptionCreationSucceeded,
+        descriptionCreationFailed
+      );
+    }
+
+    return this;
+  },
+
+  receiveRequest: function(request) {
+
+    // ISC RECEIVE REQUEST
+
+    function confirmSession() {
+      /* jshint validthis:true */
+      var contentType;
+
+      SIP.Timers.clearTimeout(this.timers.ackTimer);
+      SIP.Timers.clearTimeout(this.timers.invite2xxTimer);
+      this.status = C.STATUS_CONFIRMED;
+      this.unmute();
+
+      // TODO - this logic assumes Content-Disposition defaults
+      contentType = request.getHeader('Content-Type');
+      if (!this.mediaHandler.hasDescription(request)) {
+        this.renderbody = request.body;
+        this.rendertype = contentType;
+      }
+
+      this.emit('confirmed', request);
+    }
+
+    switch(request.method) {
+    case SIP.C.CANCEL:
+      /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL
+       * was in progress and that the UAC MAY continue with the session established by
+       * any 2xx response, or MAY terminate with BYE. SIP does continue with the
+       * established session. So the CANCEL is processed only if the session is not yet
+       * established.
+       */
+
+      /*
+       * Terminate the whole session in case the user didn't accept (or yet to send the answer) nor reject the
+       *request opening the session.
+       */
+      if(this.status === C.STATUS_WAITING_FOR_ANSWER ||
+         this.status === C.STATUS_WAITING_FOR_PRACK ||
+         this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK ||
+         this.status === C.STATUS_EARLY_MEDIA ||
+         this.status === C.STATUS_ANSWERED) {
+
+        this.status = C.STATUS_CANCELED;
+        this.request.reply(487);
+        this.canceled(request);
+        this.rejected(request, SIP.C.causes.CANCELED);
+        this.failed(request, SIP.C.causes.CANCELED);
+        this.terminated(request, SIP.C.causes.CANCELED);
+      }
+      break;
+    case SIP.C.ACK:
+      if(this.status === C.STATUS_WAITING_FOR_ACK) {
+        if (!this.hasAnswer) {
+          if(this.mediaHandler.hasDescription(request)) {
+            // ACK contains answer to an INVITE w/o SDP negotiation
+            this.hasAnswer = true;
+            this.mediaHandler.setDescription(request)
+            .then(
+              confirmSession.bind(this),
+              function onFailure (e) {
+                this.logger.warn(e);
+                this.terminate({
+                  statusCode: '488',
+                  reasonPhrase: 'Bad Media Description'
+                });
+                this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+                this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+              }.bind(this)
+            );
+          } else if (this.early_sdp) {
+            confirmSession.apply(this);
+          } else {
+            //TODO: Pass to mediahandler
+            this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+            this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+          }
+        } else {
+          confirmSession.apply(this);
+        }
+      }
+      break;
+    case SIP.C.PRACK:
+      if (this.status === C.STATUS_WAITING_FOR_PRACK || this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) {
+        //localMedia = session.mediaHandler.localMedia;
+        if(!this.hasAnswer) {
+          if(this.mediaHandler.hasDescription(request)) {
+            this.hasAnswer = true;
+            this.mediaHandler.setDescription(request)
+            .then(
+              function onSuccess () {
+                SIP.Timers.clearTimeout(this.timers.rel1xxTimer);
+                SIP.Timers.clearTimeout(this.timers.prackTimer);
+                request.reply(200);
+                if (this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) {
+                  this.status = C.STATUS_EARLY_MEDIA;
+                  this.accept();
+                }
+                this.status = C.STATUS_EARLY_MEDIA;
+                //REVISIT
+                this.mute();
+              }.bind(this),
+              function onFailure (e) {
+                //TODO: Send to media handler
+                this.logger.warn(e);
+                this.terminate({
+                  statusCode: '488',
+                  reasonPhrase: 'Bad Media Description'
+                });
+                this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+                this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+              }.bind(this)
+            );
+          } else {
+            this.terminate({
+              statusCode: '488',
+              reasonPhrase: 'Bad Media Description'
+            });
+            this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+            this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+          }
+        } else {
+          SIP.Timers.clearTimeout(this.timers.rel1xxTimer);
+          SIP.Timers.clearTimeout(this.timers.prackTimer);
+          request.reply(200);
+
+          if (this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) {
+            this.status = C.STATUS_EARLY_MEDIA;
+            this.accept();
+          }
+          this.status = C.STATUS_EARLY_MEDIA;
+          //REVISIT
+          this.mute();
+        }
+      } else if(this.status === C.STATUS_EARLY_MEDIA) {
+        request.reply(200);
+      }
+      break;
+    default:
+      Session.prototype.receiveRequest.apply(this, [request]);
+      break;
+    }
+  },
+
+  onTransportError: function() {
+    if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.CONNECTION_ERROR);
+    }
+  },
+
+  onRequestTimeout: function() {
+    if (this.status === C.STATUS_CONFIRMED) {
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    } else if (this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.REQUEST_TIMEOUT);
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    }
+  }
+
+};
+
+SIP.InviteServerContext = InviteServerContext;
+
+InviteClientContext = function(ua, target, options) {
+  options = Object.create(Session.desugar(options));
+  options.params = Object.create(options.params || Object.prototype);
+
+  var iceServers,
+    extraHeaders = (options.extraHeaders || []).slice(),
+    stunServers = options.stunServers || null,
+    turnServers = options.turnServers || null,
+    mediaHandlerFactory = options.mediaHandlerFactory || ua.configuration.mediaHandlerFactory,
+    isMediaSupported = mediaHandlerFactory.isSupported;
+
+  // Check WebRTC support
+  if (isMediaSupported && !isMediaSupported()) {
+    throw new SIP.Exceptions.NotSupportedError('Media not supported');
+  }
+
+  this.RTCConstraints = options.RTCConstraints || {};
+  this.inviteWithoutSdp = options.inviteWithoutSdp || false;
+
+  // Set anonymous property
+  this.anonymous = options.anonymous || false;
+
+  // Custom data to be sent either in INVITE or in ACK
+  this.renderbody = options.renderbody || null;
+  this.rendertype = options.rendertype || 'text/plain';
+
+  options.params.from_tag = this.from_tag;
+
+  /* Do not add ;ob in initial forming dialog requests if the registration over
+   *  the current connection got a GRUU URI.
+   */
+  this.contact = ua.contact.toString({
+    anonymous: this.anonymous,
+    outbound: this.anonymous ? !ua.contact.temp_gruu : !ua.contact.pub_gruu
+  });
+
+  if (this.anonymous) {
+    options.params.from_displayName = 'Anonymous';
+    options.params.from_uri = 'sip:anonymous@anonymous.invalid';
+
+    extraHeaders.push('P-Preferred-Identity: '+ ua.configuration.uri.toString());
+    extraHeaders.push('Privacy: id');
+  }
+  extraHeaders.push('Contact: '+ this.contact);
+  extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString());
+  if (this.inviteWithoutSdp && this.renderbody) {
+    extraHeaders.push('Content-Type: ' + this.rendertype);
+    extraHeaders.push('Content-Disposition: render;handling=optional');
+  }
+
+  if (ua.configuration.rel100 === SIP.C.supported.REQUIRED) {
+    extraHeaders.push('Require: 100rel');
+  }
+  if (ua.configuration.replaces === SIP.C.supported.REQUIRED) {
+    extraHeaders.push('Require: replaces');
+  }
+
+  options.extraHeaders = extraHeaders;
+
+  SIP.Utils.augment(this, SIP.ClientContext, [ua, SIP.C.INVITE, target, options]);
+  SIP.Utils.augment(this, SIP.Session, [mediaHandlerFactory]);
+
+  // Check Session Status
+  if (this.status !== C.STATUS_NULL) {
+    throw new SIP.Exceptions.InvalidStateError(this.status);
+  }
+
+  // Session parameter initialization
+  this.from_tag = SIP.Utils.newTag();
+
+  // OutgoingSession specific parameters
+  this.isCanceled = false;
+  this.received_100 = false;
+
+  this.method = SIP.C.INVITE;
+
+  this.receiveNonInviteResponse = this.receiveResponse;
+  this.receiveResponse = this.receiveInviteResponse;
+
+  this.logger = ua.getLogger('sip.inviteclientcontext');
+
+  if (stunServers) {
+    iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers);
+    if (!iceServers) {
+      throw new TypeError('Invalid stunServers: '+ stunServers);
+    } else {
+      this.stunServers = iceServers;
+    }
+  }
+
+  if (turnServers) {
+    iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers);
+    if (!iceServers) {
+      throw new TypeError('Invalid turnServers: '+ turnServers);
+    } else {
+      this.turnServers = iceServers;
+    }
+  }
+
+  ua.applicants[this] = this;
+
+  this.id = this.request.call_id + this.from_tag;
+
+  //Initialize Media Session
+  this.mediaHandler = this.mediaHandlerFactory(this, {
+    RTCConstraints: this.RTCConstraints,
+    stunServers: this.stunServers,
+    turnServers: this.turnServers
+  });
+
+  if (this.mediaHandler && this.mediaHandler.getRemoteStreams) {
+    this.getRemoteStreams = this.mediaHandler.getRemoteStreams.bind(this.mediaHandler);
+    this.getLocalStreams = this.mediaHandler.getLocalStreams.bind(this.mediaHandler);
+  }
+
+  SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media);
+  this.mediaHint = options.media;
+
+  this.onInfo = options.onInfo;
+};
+
+InviteClientContext.prototype = {
+  invite: function () {
+    var self = this;
+
+    //Save the session into the ua sessions collection.
+    //Note: placing in constructor breaks call to request.cancel on close... User does not need this anyway
+    this.ua.sessions[this.id] = this;
+
+    //Note: due to the way Firefox handles gUM calls, it is recommended to make the gUM call at the app level
+    // and hand sip.js a stream as the mediaHint
+    if (this.inviteWithoutSdp) {
+      //just send an invite with no sdp...
+      this.request.body = self.renderbody;
+      this.status = C.STATUS_INVITE_SENT;
+      this.send();
+    } else {
+      this.mediaHandler.getDescription(self.mediaHint)
+      .then(
+        function onSuccess(description) {
+          if (self.isCanceled || self.status === C.STATUS_TERMINATED) {
+            return;
+          }
+          self.hasOffer = true;
+          self.request.body = description;
+          self.status = C.STATUS_INVITE_SENT;
+          self.send();
+        },
+        function onFailure() {
+          if (self.status === C.STATUS_TERMINATED) {
+            return;
+          }
+          // TODO...fail out
+          //self.failed(null, SIP.C.causes.USER_DENIED_MEDIA_ACCESS);
+          //self.failed(null, SIP.C.causes.WEBRTC_ERROR);
+          self.failed(null, SIP.C.causes.WEBRTC_ERROR);
+          self.terminated(null, SIP.C.causes.WEBRTC_ERROR);
+        }
+      );
+    }
+
+    return this;
+  },
+
+  receiveInviteResponse: function(response) {
+    var cause, //localMedia,
+      session = this,
+      id = response.call_id + response.from_tag + response.to_tag,
+      extraHeaders = [],
+      options = {};
+
+    if (this.status === C.STATUS_TERMINATED || response.method !== SIP.C.INVITE) {
+      return;
+    }
+
+    if (this.dialog && (response.status_code >= 200 && response.status_code <= 299)) {
+      if (id !== this.dialog.id.toString() ) {
+        if (!this.createDialog(response, 'UAC', true)) {
+          return;
+        }
+        this.earlyDialogs[id].sendRequest(this, SIP.C.ACK,
+                                          {
+                                            body: SIP.Utils.generateFakeSDP(response.body)
+                                          });
+        this.earlyDialogs[id].sendRequest(this, SIP.C.BYE);
+
+        /* NOTE: This fails because the forking proxy does not recognize that an unanswerable
+         * leg (due to peerConnection limitations) has been answered first. If your forking
+         * proxy does not hang up all unanswered branches on the first branch answered, remove this.
+         */
+        if(this.status !== C.STATUS_CONFIRMED) {
+          this.failed(response, SIP.C.causes.WEBRTC_ERROR);
+          this.terminated(response, SIP.C.causes.WEBRTC_ERROR);
+        }
+        return;
+      } else if (this.status === C.STATUS_CONFIRMED) {
+        this.sendRequest(SIP.C.ACK,{cseq: response.cseq});
+        return;
+      } else if (!this.hasAnswer) {
+        // invite w/o sdp is waiting for callback
+        //an invite with sdp must go on, and hasAnswer is true
+        return;
+      }
+    }
+
+    if (this.dialog && response.status_code < 200) {
+      /*
+        Early media has been set up with at least one other different branch,
+        but a final 2xx response hasn't been received
+      */
+      if (this.dialog.pracked.indexOf(response.getHeader('rseq')) !== -1 ||
+          (this.dialog.pracked[this.dialog.pracked.length-1] >= response.getHeader('rseq') && this.dialog.pracked.length > 0)) {
+        return;
+      }
+
+      if (!this.earlyDialogs[id] && !this.createDialog(response, 'UAC', true)) {
+        return;
+      }
+
+      if (this.earlyDialogs[id].pracked.indexOf(response.getHeader('rseq')) !== -1 ||
+          (this.earlyDialogs[id].pracked[this.earlyDialogs[id].pracked.length-1] >= response.getHeader('rseq') && this.earlyDialogs[id].pracked.length > 0)) {
+        return;
+      }
+
+      extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
+      this.earlyDialogs[id].pracked.push(response.getHeader('rseq'));
+
+      this.earlyDialogs[id].sendRequest(this, SIP.C.PRACK, {
+        extraHeaders: extraHeaders,
+        body: SIP.Utils.generateFakeSDP(response.body)
+      });
+      return;
+    }
+
+    // Proceed to cancellation if the user requested.
+    if(this.isCanceled) {
+      if(response.status_code >= 100 && response.status_code < 200) {
+        this.request.cancel(this.cancelReason, extraHeaders);
+        this.canceled(null);
+      } else if(response.status_code >= 200 && response.status_code < 299) {
+        this.acceptAndTerminate(response);
+        this.emit('bye', this.request);
+      } else if (response.status_code >= 300) {
+        cause = SIP.C.REASON_PHRASE[response.status_code] || SIP.C.causes.CANCELED;
+        this.rejected(response, cause);
+        this.failed(response, cause);
+        this.terminated(response, cause);
+      }
+      return;
+    }
+
+    switch(true) {
+      case /^100$/.test(response.status_code):
+        this.received_100 = true;
+        this.emit('progress', response);
+        break;
+      case (/^1[0-9]{2}$/.test(response.status_code)):
+        // Do nothing with 1xx responses without To tag.
+        if(!response.to_tag) {
+          this.logger.warn('1xx response received without to tag');
+          break;
+        }
+
+        // Create Early Dialog if 1XX comes with contact
+        if(response.hasHeader('contact')) {
+          // An error on dialog creation will fire 'failed' event
+          if (!this.createDialog(response, 'UAC', true)) {
+            break;
+          }
+        }
+
+        this.status = C.STATUS_1XX_RECEIVED;
+
+        if(response.hasHeader('require') &&
+           response.getHeader('require').indexOf('100rel') !== -1) {
+
+          // Do nothing if this.dialog is already confirmed
+          if (this.dialog || !this.earlyDialogs[id]) {
+            break;
+          }
+
+          if (this.earlyDialogs[id].pracked.indexOf(response.getHeader('rseq')) !== -1 ||
+              (this.earlyDialogs[id].pracked[this.earlyDialogs[id].pracked.length-1] >= response.getHeader('rseq') && this.earlyDialogs[id].pracked.length > 0)) {
+            return;
+          }
+
+          if (!this.mediaHandler.hasDescription(response)) {
+            extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
+            this.earlyDialogs[id].pracked.push(response.getHeader('rseq'));
+            this.earlyDialogs[id].sendRequest(this, SIP.C.PRACK, {
+              extraHeaders: extraHeaders
+            });
+            this.emit('progress', response);
+
+          } else if (this.hasOffer) {
+            if (!this.createDialog(response, 'UAC')) {
+              break;
+            }
+            this.hasAnswer = true;
+            this.dialog.pracked.push(response.getHeader('rseq'));
+
+            this.mediaHandler.setDescription(response)
+            .then(
+              function onSuccess () {
+                extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
+
+                session.sendRequest(SIP.C.PRACK, {
+                  extraHeaders: extraHeaders,
+                  receiveResponse: function() {}
+                });
+                session.status = C.STATUS_EARLY_MEDIA;
+                session.mute();
+                session.emit('progress', response);
+                /*
+                if (session.status === C.STATUS_EARLY_MEDIA) {
+                  localMedia = session.mediaHandler.localMedia;
+                  if (localMedia.getAudioTracks().length > 0) {
+                    localMedia.getAudioTracks()[0].enabled = false;
+                  }
+                  if (localMedia.getVideoTracks().length > 0) {
+                    localMedia.getVideoTracks()[0].enabled = false;
+                  }
+                }*/
+              },
+              function onFailure (e) {
+                session.logger.warn(e);
+                session.acceptAndTerminate(response, 488, 'Not Acceptable Here');
+                session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+              }
+            );
+          } else {
+            var earlyDialog = this.earlyDialogs[id];
+            var earlyMedia = earlyDialog.mediaHandler;
+
+            earlyDialog.pracked.push(response.getHeader('rseq'));
+
+            earlyMedia.setDescription(response)
+            .then(earlyMedia.getDescription.bind(earlyMedia, session.mediaHint))
+            .then(function onSuccess(description) {
+              extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
+              earlyDialog.sendRequest(session, SIP.C.PRACK, {
+                extraHeaders: extraHeaders,
+                body: description
+              });
+              session.status = C.STATUS_EARLY_MEDIA;
+              session.emit('progress', response);
+            })
+            .catch(function onFailure(e) {
+              if (e instanceof SIP.Exceptions.GetDescriptionError) {
+                earlyDialog.pracked.push(response.getHeader('rseq'));
+                if (session.status === C.STATUS_TERMINATED) {
+                  return;
+                }
+                // TODO - fail out on error
+                // session.failed(gum error);
+                session.failed(null, SIP.C.causes.WEBRTC_ERROR);
+                session.terminated(null, SIP.C.causes.WEBRTC_ERROR);
+              } else {
+                earlyDialog.pracked.splice(earlyDialog.pracked.indexOf(response.getHeader('rseq')), 1);
+                // Could not set remote description
+                session.logger.warn('invalid description');
+                session.logger.warn(e);
+              }
+            });
+          }
+        } else {
+          this.emit('progress', response);
+        }
+        break;
+      case /^2[0-9]{2}$/.test(response.status_code):
+        var cseq = this.request.cseq + ' ' + this.request.method;
+        if (cseq !== response.getHeader('cseq')) {
+          break;
+        }
+
+        if (this.status === C.STATUS_EARLY_MEDIA && this.dialog) {
+          this.status = C.STATUS_CONFIRMED;
+          this.unmute();
+          /*localMedia = this.mediaHandler.localMedia;
+          if (localMedia.getAudioTracks().length > 0) {
+            localMedia.getAudioTracks()[0].enabled = true;
+          }
+          if (localMedia.getVideoTracks().length > 0) {
+            localMedia.getVideoTracks()[0].enabled = true;
+          }*/
+          options = {};
+          if (this.renderbody) {
+            extraHeaders.push('Content-Type: ' + this.rendertype);
+            options.extraHeaders = extraHeaders;
+            options.body = this.renderbody;
+          }
+          options.cseq = response.cseq;
+          this.sendRequest(SIP.C.ACK, options);
+          this.accepted(response);
+          break;
+        }
+        // Do nothing if this.dialog is already confirmed
+        if (this.dialog) {
+          break;
+        }
+
+        // This is an invite without sdp
+        if (!this.hasOffer) {
+          if (this.earlyDialogs[id] && this.earlyDialogs[id].mediaHandler.localMedia) {
+            //REVISIT
+            this.hasOffer = true;
+            this.hasAnswer = true;
+            this.mediaHandler = this.earlyDialogs[id].mediaHandler;
+            if (!this.createDialog(response, 'UAC')) {
+              break;
+            }
+            this.status = C.STATUS_CONFIRMED;
+            this.sendRequest(SIP.C.ACK, {cseq:response.cseq});
+
+            this.unmute();
+            /*
+            localMedia = session.mediaHandler.localMedia;
+            if (localMedia.getAudioTracks().length > 0) {
+              localMedia.getAudioTracks()[0].enabled = true;
+            }
+            if (localMedia.getVideoTracks().length > 0) {
+              localMedia.getVideoTracks()[0].enabled = true;
+            }*/
+            this.accepted(response);
+          } else {
+            if(!this.mediaHandler.hasDescription(response)) {
+              this.acceptAndTerminate(response, 400, 'Missing session description');
+              this.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+              break;
+            }
+            if (!this.createDialog(response, 'UAC')) {
+              break;
+            }
+            this.hasOffer = true;
+            this.mediaHandler.setDescription(response)
+            .then(this.mediaHandler.getDescription.bind(this.mediaHandler, this.mediaHint))
+            .then(function onSuccess(description) {
+              //var localMedia;
+              if(session.isCanceled || session.status === C.STATUS_TERMINATED) {
+                return;
+              }
+
+              session.status = C.STATUS_CONFIRMED;
+              session.hasAnswer = true;
+
+              session.unmute();
+              /*localMedia = session.mediaHandler.localMedia;
+              if (localMedia.getAudioTracks().length > 0) {
+                localMedia.getAudioTracks()[0].enabled = true;
+              }
+              if (localMedia.getVideoTracks().length > 0) {
+                localMedia.getVideoTracks()[0].enabled = true;
+              }*/
+              session.sendRequest(SIP.C.ACK,{
+                body: description,
+                cseq:response.cseq
+              });
+              session.accepted(response);
+            })
+            .catch(function onFailure(e) {
+              if (e instanceof SIP.Exceptions.GetDescriptionError) {
+                // TODO do something here
+                session.logger.warn("there was a problem");
+              } else {
+                session.logger.warn('invalid description');
+                session.logger.warn(e);
+                session.acceptAndTerminate(response, 488, 'Invalid session description');
+                session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+              }
+            });
+          }
+        } else if (this.hasAnswer){
+          if (this.renderbody) {
+            extraHeaders.push('Content-Type: ' + session.rendertype);
+            options.extraHeaders = extraHeaders;
+            options.body = this.renderbody;
+          }
+          this.sendRequest(SIP.C.ACK, options);
+        } else {
+          if(!this.mediaHandler.hasDescription(response)) {
+            this.acceptAndTerminate(response, 400, 'Missing session description');
+            this.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+            break;
+          }
+          if (!this.createDialog(response, 'UAC')) {
+            break;
+          }
+          this.hasAnswer = true;
+          this.mediaHandler.setDescription(response)
+          .then(
+            function onSuccess () {
+              var options = {};//,localMedia;
+              session.status = C.STATUS_CONFIRMED;
+              session.unmute();
+              /*localMedia = session.mediaHandler.localMedia;
+              if (localMedia.getAudioTracks().length > 0) {
+                localMedia.getAudioTracks()[0].enabled = true;
+              }
+              if (localMedia.getVideoTracks().length > 0) {
+                localMedia.getVideoTracks()[0].enabled = true;
+              }*/
+              if (session.renderbody) {
+                extraHeaders.push('Content-Type: ' + session.rendertype);
+                options.extraHeaders = extraHeaders;
+                options.body = session.renderbody;
+              }
+              options.cseq = response.cseq;
+              session.sendRequest(SIP.C.ACK, options);
+              session.accepted(response);
+            },
+            function onFailure (e) {
+              session.logger.warn(e);
+              session.acceptAndTerminate(response, 488, 'Not Acceptable Here');
+              session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
+            }
+          );
+        }
+        break;
+      default:
+        cause = SIP.Utils.sipErrorCause(response.status_code);
+        this.rejected(response, cause);
+        this.failed(response, cause);
+        this.terminated(response, cause);
+    }
+  },
+
+  cancel: function(options) {
+    options = options || {};
+
+    options.extraHeaders = (options.extraHeaders || []).slice();
+
+    // Check Session Status
+    if (this.status === C.STATUS_TERMINATED || this.status === C.STATUS_CONFIRMED) {
+      throw new SIP.Exceptions.InvalidStateError(this.status);
+    }
+
+    this.logger.log('canceling RTCSession');
+
+    var cancel_reason = SIP.Utils.getCancelReason(options.status_code, options.reason_phrase);
+
+    // Check Session Status
+    if (this.status === C.STATUS_NULL ||
+        (this.status === C.STATUS_INVITE_SENT && !this.received_100)) {
+      this.isCanceled = true;
+      this.cancelReason = cancel_reason;
+    } else if (this.status === C.STATUS_INVITE_SENT ||
+               this.status === C.STATUS_1XX_RECEIVED ||
+               this.status === C.STATUS_EARLY_MEDIA) {
+      this.request.cancel(cancel_reason, options.extraHeaders);
+    }
+
+    return this.canceled();
+  },
+
+  terminate: function(options) {
+    if (this.status === C.STATUS_TERMINATED) {
+      return this;
+    }
+
+    if (this.status === C.STATUS_WAITING_FOR_ACK || this.status === C.STATUS_CONFIRMED) {
+      this.bye(options);
+    } else {
+      this.cancel(options);
+    }
+
+    return this;
+  },
+
+  receiveRequest: function(request) {
+    // ICC RECEIVE REQUEST
+
+    // Reject CANCELs
+    if (request.method === SIP.C.CANCEL) {
+      // TODO; make this a switch when it gets added
+    }
+
+    if (request.method === SIP.C.ACK && this.status === C.STATUS_WAITING_FOR_ACK) {
+      SIP.Timers.clearTimeout(this.timers.ackTimer);
+      SIP.Timers.clearTimeout(this.timers.invite2xxTimer);
+      this.status = C.STATUS_CONFIRMED;
+      this.unmute();
+
+      this.accepted();
+    }
+
+    return Session.prototype.receiveRequest.apply(this, [request]);
+  },
+
+  onTransportError: function() {
+    if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.CONNECTION_ERROR);
+    }
+  },
+
+  onRequestTimeout: function() {
+    if (this.status === C.STATUS_CONFIRMED) {
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    } else if (this.status !== C.STATUS_TERMINATED) {
+      this.failed(null, SIP.C.causes.REQUEST_TIMEOUT);
+      this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT);
+    }
+  }
+
+};
+
+SIP.InviteClientContext = InviteClientContext;
+
+};
+
+},{"./Session/DTMF":24}],24:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview DTMF
+ */
+
+/**
+ * @class DTMF
+ * @param {SIP.Session} session
+ */
+module.exports = function (SIP) {
+
+var DTMF,
+  C = {
+    MIN_DURATION:            70,
+    MAX_DURATION:            6000,
+    DEFAULT_DURATION:        100,
+    MIN_INTER_TONE_GAP:      50,
+    DEFAULT_INTER_TONE_GAP:  500
+  };
+
+DTMF = function(session, tone, options) {
+  var duration, interToneGap;
+
+  if (tone === undefined) {
+    throw new TypeError('Not enough arguments');
+  }
+
+  this.logger = session.ua.getLogger('sip.invitecontext.dtmf', session.id);
+  this.owner = session;
+  this.direction = null;
+
+  options = options || {};
+  duration = options.duration || null;
+  interToneGap = options.interToneGap || null;
+
+  // Check tone type
+  if (typeof tone === 'string' ) {
+    tone = tone.toUpperCase();
+  } else if (typeof tone === 'number') {
+    tone = tone.toString();
+  } else {
+    throw new TypeError('Invalid tone: '+ tone);
+  }
+
+  // Check tone value
+  if (!tone.match(/^[0-9A-D#*]$/)) {
+    throw new TypeError('Invalid tone: '+ tone);
+  } else {
+    this.tone = tone;
+  }
+
+  // Check duration
+  if (duration && !SIP.Utils.isDecimal(duration)) {
+    throw new TypeError('Invalid tone duration: '+ duration);
+  } else if (!duration) {
+    duration = DTMF.C.DEFAULT_DURATION;
+  } else if (duration < DTMF.C.MIN_DURATION) {
+    this.logger.warn('"duration" value is lower than the minimum allowed, setting it to '+ DTMF.C.MIN_DURATION+ ' milliseconds');
+    duration = DTMF.C.MIN_DURATION;
+  } else if (duration > DTMF.C.MAX_DURATION) {
+    this.logger.warn('"duration" value is greater than the maximum allowed, setting it to '+ DTMF.C.MAX_DURATION +' milliseconds');
+    duration = DTMF.C.MAX_DURATION;
+  } else {
+    duration = Math.abs(duration);
+  }
+  this.duration = duration;
+
+  // Check interToneGap
+  if (interToneGap && !SIP.Utils.isDecimal(interToneGap)) {
+    throw new TypeError('Invalid interToneGap: '+ interToneGap);
+  } else if (!interToneGap) {
+    interToneGap = DTMF.C.DEFAULT_INTER_TONE_GAP;
+  } else if (interToneGap < DTMF.C.MIN_INTER_TONE_GAP) {
+    this.logger.warn('"interToneGap" value is lower than the minimum allowed, setting it to '+ DTMF.C.MIN_INTER_TONE_GAP +' milliseconds');
+    interToneGap = DTMF.C.MIN_INTER_TONE_GAP;
+  } else {
+    interToneGap = Math.abs(interToneGap);
+  }
+  this.interToneGap = interToneGap;
+};
+DTMF.prototype = Object.create(SIP.EventEmitter.prototype);
+
+
+DTMF.prototype.send = function(options) {
+  var extraHeaders,
+    body = {};
+
+  this.direction = 'outgoing';
+
+  // Check RTCSession Status
+  if (this.owner.status !== SIP.Session.C.STATUS_CONFIRMED &&
+    this.owner.status !== SIP.Session.C.STATUS_WAITING_FOR_ACK) {
+    throw new SIP.Exceptions.InvalidStateError(this.owner.status);
+  }
+
+  // Get DTMF options
+  options = options || {};
+  extraHeaders = options.extraHeaders ? options.extraHeaders.slice() : [];
+
+  body.contentType = 'application/dtmf-relay';
+
+  body.body = "Signal= " + this.tone + "\r\n";
+  body.body += "Duration= " + this.duration;
+
+  this.request = this.owner.dialog.sendRequest(this, SIP.C.INFO, {
+    extraHeaders: extraHeaders,
+    body: body
+  });
+
+  this.owner.emit('dtmf', this.request, this);
+};
+
+/**
+ * @private
+ */
+DTMF.prototype.receiveResponse = function(response) {
+  var cause;
+
+  switch(true) {
+    case /^1[0-9]{2}$/.test(response.status_code):
+      // Ignore provisional responses.
+      break;
+
+    case /^2[0-9]{2}$/.test(response.status_code):
+      this.emit('succeeded', {
+        originator: 'remote',
+        response: response
+      });
+      break;
+
+    default:
+      cause = SIP.Utils.sipErrorCause(response.status_code);
+      this.emit('failed', response, cause);
+      break;
+  }
+};
+
+/**
+ * @private
+ */
+DTMF.prototype.onRequestTimeout = function() {
+  this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT);
+  this.owner.onRequestTimeout();
+};
+
+/**
+ * @private
+ */
+DTMF.prototype.onTransportError = function() {
+  this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR);
+  this.owner.onTransportError();
+};
+
+/**
+ * @private
+ */
+DTMF.prototype.onDialogError = function(response) {
+  this.emit('failed', response, SIP.C.causes.DIALOG_ERROR);
+  this.owner.onDialogError(response);
+};
+
+/**
+ * @private
+ */
+DTMF.prototype.init_incoming = function(request) {
+  this.direction = 'incoming';
+  this.request = request;
+
+  request.reply(200);
+
+  if (!this.tone || !this.duration) {
+    this.logger.warn('invalid INFO DTMF received, discarded');
+  } else {
+    this.owner.emit('dtmf', request, this);
+  }
+};
+
+DTMF.C = C;
+return DTMF;
+};
+
+},{}],25:[function(require,module,exports){
+"use strict";
+
+/**
+ * @fileoverview SIP Subscriber (SIP-Specific Event Notifications RFC6665)
+ */
+
+/**
+ * @augments SIP
+ * @class Class creating a SIP Subscription.
+ */
+module.exports = function (SIP) {
+SIP.Subscription = function (ua, target, event, options) {
+  options = Object.create(options || Object.prototype);
+  this.extraHeaders = options.extraHeaders = (options.extraHeaders || []).slice();
+
+  this.id = null;
+  this.state = 'init';
+
+  if (!event) {
+    throw new TypeError('Event necessary to create a subscription.');
+  } else {
+    //TODO: check for valid events here probably make a list in SIP.C; or leave it up to app to check?
+    //The check may need to/should probably occur on the other side,
+    this.event = event;
+  }
+
+  if(typeof options.expires !== 'number'){
+    ua.logger.warn('expires must be a number. Using default of 3600.');
+    this.expires = 3600;
+  } else {
+    this.expires = options.expires;
+  }
+
+  options.extraHeaders.push('Event: ' + this.event);
+  options.extraHeaders.push('Expires: ' + this.expires);
+
+  if (options.body) {
+    this.body = options.body;
+  }
+
+  this.contact = ua.contact.toString();
+
+  options.extraHeaders.push('Contact: '+ this.contact);
+  options.extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString());
+
+  SIP.Utils.augment(this, SIP.ClientContext, [ua, SIP.C.SUBSCRIBE, target, options]);
+
+  this.logger = ua.getLogger('sip.subscription');
+
+  this.dialog = null;
+  this.timers = {N: null, sub_duration: null};
+  this.errorCodes  = [404,405,410,416,480,481,482,483,484,485,489,501,604];
+};
+
+SIP.Subscription.prototype = {
+  subscribe: function() {
+    var sub = this;
+
+     //these states point to an existing subscription, no subscribe is necessary
+    if (this.state === 'active') {
+      this.refresh();
+      return this;
+    } else if (this.state === 'notify_wait') {
+      return this;
+    }
+
+    SIP.Timers.clearTimeout(this.timers.sub_duration);
+    SIP.Timers.clearTimeout(this.timers.N);
+    this.timers.N = SIP.Timers.setTimeout(sub.timer_fire.bind(sub), SIP.Timers.TIMER_N);
+
+    this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event] = this;
+
+    this.send();
+
+    this.state = 'notify_wait';
+
+    return this;
+  },
+
+  refresh: function () {
+    if (this.state === 'terminated' || this.state === 'pending' || this.state === 'notify_wait') {
+      return;
+    }
+
+    this.dialog.sendRequest(this, SIP.C.SUBSCRIBE, {
+      extraHeaders: this.extraHeaders,
+      body: this.body
+    });
+  },
+
+  receiveResponse: function(response) {
+    var expires, sub = this,
+        cause = SIP.Utils.getReasonPhrase(response.status_code);
+
+    if ((this.state === 'notify_wait' && response.status_code >= 300) ||
+        (this.state !== 'notify_wait' && this.errorCodes.indexOf(response.status_code) !== -1)) {
+      this.failed(response, null);
+    } else if (/^2[0-9]{2}$/.test(response.status_code)){
+      this.emit('accepted', response, cause);
+      //As we don't support RFC 5839 or other extensions where the NOTIFY is optional, timer N will not be cleared
+      //SIP.Timers.clearTimeout(this.timers.N);
+
+      expires = response.getHeader('Expires');
+
+      if (expires && expires <= this.expires) {
+        // Preserve new expires value for subsequent requests
+        this.expires = expires;
+        this.timers.sub_duration = SIP.Timers.setTimeout(sub.refresh.bind(sub), expires * 900);
+      } else {
+        if (!expires) {
+          this.logger.warn('Expires header missing in a 200-class response to SUBSCRIBE');
+          this.failed(response, SIP.C.EXPIRES_HEADER_MISSING);
+        } else {
+          this.logger.warn('Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request');
+          this.failed(response, SIP.C.INVALID_EXPIRES_HEADER);
+        }
+      }
+    } else if (response.statusCode > 300) {
+      this.emit('failed', response, cause);
+      this.emit('rejected', response, cause);
+    }
+  },
+
+  unsubscribe: function() {
+    var extraHeaders = [], sub = this;
+
+    this.state = 'terminated';
+
+    extraHeaders.push('Event: ' + this.event);
+    extraHeaders.push('Expires: 0');
+
+    extraHeaders.push('Contact: '+ this.contact);
+    extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString());
+
+    //makes sure expires isn't set, and other typical resubscribe behavior
+    this.receiveResponse = function(){};
+
+    this.dialog.sendRequest(this, this.method, {
+      extraHeaders: extraHeaders,
+      body: this.body
+    });
+
+    SIP.Timers.clearTimeout(this.timers.sub_duration);
+    SIP.Timers.clearTimeout(this.timers.N);
+    this.timers.N = SIP.Timers.setTimeout(sub.timer_fire.bind(sub), SIP.Timers.TIMER_N);
+  },
+
+  /**
+  * @private
+  */
+  timer_fire: function(){
+    if (this.state === 'terminated') {
+      this.terminateDialog();
+      SIP.Timers.clearTimeout(this.timers.N);
+      SIP.Timers.clearTimeout(this.timers.sub_duration);
+
+      delete this.ua.subscriptions[this.id];
+    } else if (this.state === 'notify_wait' || this.state === 'pending') {
+      this.close();
+    } else {
+      this.refresh();
+    }
+  },
+
+  /**
+  * @private
+  */
+  close: function() {
+    if (this.state === 'notify_wait') {
+      this.state = 'terminated';
+      SIP.Timers.clearTimeout(this.timers.N);
+      SIP.Timers.clearTimeout(this.timers.sub_duration);
+      this.receiveResponse = function(){};
+
+      delete this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event];
+    } else if (this.state !== 'terminated') {
+      this.unsubscribe();
+    }
+  },
+
+  /**
+  * @private
+  */
+  createConfirmedDialog: function(message, type) {
+    var dialog;
+
+    this.terminateDialog();
+    dialog = new SIP.Dialog(this, message, type);
+    dialog.invite_seqnum = this.request.cseq;
+    dialog.local_seqnum = this.request.cseq;
+
+    if(!dialog.error) {
+      this.dialog = dialog;
+      return true;
+    }
+    // Dialog not created due to an error
+    else {
+      return false;
+    }
+  },
+
+  /**
+  * @private
+  */
+  terminateDialog: function() {
+    if(this.dialog) {
+      delete this.ua.subscriptions[this.id];
+      this.dialog.terminate();
+      delete this.dialog;
+    }
+  },
+
+  /**
+  * @private
+  */
+  receiveRequest: function(request) {
+    var sub_state, sub = this;
+
+    function setExpiresTimeout() {
+      if (sub_state.expires) {
+        SIP.Timers.clearTimeout(sub.timers.sub_duration);
+        sub_state.expires = Math.min(sub.expires,
+                                     Math.max(sub_state.expires, 0));
+        sub.timers.sub_duration = SIP.Timers.setTimeout(sub.refresh.bind(sub),
+                                                    sub_state.expires * 900);
+      }
+    }
+
+    if (!this.matchEvent(request)) { //checks event and subscription_state headers
+      request.reply(489);
+      return;
+    }
+
+    if (!this.dialog) {
+      if (this.createConfirmedDialog(request,'UAS')) {
+        this.id = this.dialog.id.toString();
+        delete this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event];
+        this.ua.subscriptions[this.id] = this;
+        // UPDATE ROUTE SET TO BE BACKWARDS COMPATIBLE?
+      }
+    }
+
+    sub_state = request.parseHeader('Subscription-State');
+
+    request.reply(200, SIP.C.REASON_200);
+
+    SIP.Timers.clearTimeout(this.timers.N);
+
+    this.emit('notify', {request: request});
+
+    // if we've set state to terminated, no further processing should take place
+    // and we are only interested in cleaning up after the appropriate NOTIFY
+    if (this.state === 'terminated') {
+      if (sub_state.state === 'terminated') {
+        this.terminateDialog();
+        SIP.Timers.clearTimeout(this.timers.N);
+        SIP.Timers.clearTimeout(this.timers.sub_duration);
+
+        delete this.ua.subscriptions[this.id];
+      }
+      return;
+    }
+
+    switch (sub_state.state) {
+      case 'active':
+        this.state = 'active';
+        setExpiresTimeout();
+        break;
+      case 'pending':
+        if (this.state === 'notify_wait') {
+          setExpiresTimeout();
+        }
+        this.state = 'pending';
+        break;
+      case 'terminated':
+        SIP.Timers.clearTimeout(this.timers.sub_duration);
+        if (sub_state.reason) {
+          this.logger.log('terminating subscription with reason '+ sub_state.reason);
+          switch (sub_state.reason) {
+            case 'deactivated':
+            case 'timeout':
+              this.subscribe();
+              return;
+            case 'probation':
+            case 'giveup':
+              if(sub_state.params && sub_state.params['retry-after']) {
+                this.timers.sub_duration = SIP.Timers.setTimeout(sub.subscribe.bind(sub), sub_state.params['retry-after']);
+              } else {
+                this.subscribe();
+              }
+              return;
+            case 'rejected':
+            case 'noresource':
+            case 'invariant':
+              break;
+          }
+        }
+        this.close();
+        break;
+    }
+  },
+
+  failed: function(response, cause) {
+    this.close();
+    this.emit('failed', response, cause);
+    this.emit('rejected', response, cause);
+    return this;
+  },
+
+  onDialogError: function(response) {
+    this.failed(response, SIP.C.causes.DIALOG_ERROR);
+  },
+
+  /**
+  * @private
+  */
+  matchEvent: function(request) {
+    var event;
+
+    // Check mandatory header Event
+    if (!request.hasHeader('Event')) {
+      this.logger.warn('missing Event header');
+      return false;
+    }
+    // Check mandatory header Subscription-State
+    if (!request.hasHeader('Subscription-State')) {
+      this.logger.warn('missing Subscription-State header');
+      return false;
+    }
+
+    // Check whether the event in NOTIFY matches the event in SUBSCRIBE
+    event = request.parseHeader('event').event;
+
+    if (this.event !== event) {
+      this.logger.warn('event match failed');
+      request.reply(481, 'Event Match Failed');
+      return false;
+    } else {
+      return true;
+    }
+  }
+};
+};
+
+},{}],26:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP TIMERS
+ */
+
+/**
+ * @augments SIP
+ */
+var
+  T1 = 500,
+  T2 = 4000,
+  T4 = 5000;
+module.exports = function (timers) {
+  var Timers = {
+    T1: T1,
+    T2: T2,
+    T4: T4,
+    TIMER_B: 64 * T1,
+    TIMER_D: 0  * T1,
+    TIMER_F: 64 * T1,
+    TIMER_H: 64 * T1,
+    TIMER_I: 0  * T1,
+    TIMER_J: 0  * T1,
+    TIMER_K: 0  * T4,
+    TIMER_L: 64 * T1,
+    TIMER_M: 64 * T1,
+    TIMER_N: 64 * T1,
+    PROVISIONAL_RESPONSE_INTERVAL: 60000  // See RFC 3261 Section 13.3.1.1
+  };
+
+  ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval']
+  .forEach(function (name) {
+    // can't just use timers[name].bind(timers) since it bypasses jasmine's
+    // clock-mocking
+    Timers[name] = function () {
+      return timers[name].apply(timers, arguments);
+    };
+  });
+
+  return Timers;
+};
+
+},{}],27:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP Transactions
+ */
+
+/**
+ * SIP Transactions module.
+ * @augments SIP
+ */
+module.exports = function (SIP) {
+var
+  C = {
+    // Transaction states
+    STATUS_TRYING:     1,
+    STATUS_PROCEEDING: 2,
+    STATUS_CALLING:    3,
+    STATUS_ACCEPTED:   4,
+    STATUS_COMPLETED:  5,
+    STATUS_TERMINATED: 6,
+    STATUS_CONFIRMED:  7,
+
+    // Transaction types
+    NON_INVITE_CLIENT: 'nict',
+    NON_INVITE_SERVER: 'nist',
+    INVITE_CLIENT: 'ict',
+    INVITE_SERVER: 'ist'
+  };
+
+function buildViaHeader (request_sender, transport, id) {
+  var via;
+  via = 'SIP/2.0/' + (request_sender.ua.configuration.hackViaTcp ? 'TCP' : transport.server.scheme);
+  via += ' ' + request_sender.ua.configuration.viaHost + ';branch=' + id;
+  if (request_sender.ua.configuration.forceRport) {
+    via += ';rport';
+  }
+  return via;
+}
+
+/**
+* @augments SIP.Transactions
+* @class Non Invite Client Transaction
+* @param {SIP.RequestSender} request_sender
+* @param {SIP.OutgoingRequest} request
+* @param {SIP.Transport} transport
+*/
+var NonInviteClientTransaction = function(request_sender, request, transport) {
+  var via;
+
+  this.type = C.NON_INVITE_CLIENT;
+  this.transport = transport;
+  this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000);
+  this.request_sender = request_sender;
+  this.request = request;
+
+  this.logger = request_sender.ua.getLogger('sip.transaction.nict', this.id);
+
+  via = buildViaHeader(request_sender, transport, this.id);
+  this.request.setHeader('via', via);
+
+  this.request_sender.ua.newTransaction(this);
+};
+NonInviteClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype);
+
+NonInviteClientTransaction.prototype.stateChanged = function(state) {
+  this.state = state;
+  this.emit('stateChanged');
+};
+
+NonInviteClientTransaction.prototype.send = function() {
+  var tr = this;
+
+  this.stateChanged(C.STATUS_TRYING);
+  this.F = SIP.Timers.setTimeout(tr.timer_F.bind(tr), SIP.Timers.TIMER_F);
+
+  if(!this.transport.send(this.request)) {
+    this.onTransportError();
+  }
+};
+
+NonInviteClientTransaction.prototype.onTransportError = function() {
+  this.logger.log('transport error occurred, deleting non-INVITE client transaction ' + this.id);
+  SIP.Timers.clearTimeout(this.F);
+  SIP.Timers.clearTimeout(this.K);
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.request_sender.ua.destroyTransaction(this);
+  this.request_sender.onTransportError();
+};
+
+NonInviteClientTransaction.prototype.timer_F = function() {
+  this.logger.log('Timer F expired for non-INVITE client transaction ' + this.id);
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.request_sender.ua.destroyTransaction(this);
+  this.request_sender.onRequestTimeout();
+};
+
+NonInviteClientTransaction.prototype.timer_K = function() {
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.request_sender.ua.destroyTransaction(this);
+};
+
+NonInviteClientTransaction.prototype.receiveResponse = function(response) {
+  var
+    tr = this,
+    status_code = response.status_code;
+
+  if(status_code < 200) {
+    switch(this.state) {
+      case C.STATUS_TRYING:
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_PROCEEDING);
+        this.request_sender.receiveResponse(response);
+        break;
+    }
+  } else {
+    switch(this.state) {
+      case C.STATUS_TRYING:
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_COMPLETED);
+        SIP.Timers.clearTimeout(this.F);
+
+        if(status_code === 408) {
+          this.request_sender.onRequestTimeout();
+        } else {
+          this.request_sender.receiveResponse(response);
+        }
+
+        this.K = SIP.Timers.setTimeout(tr.timer_K.bind(tr), SIP.Timers.TIMER_K);
+        break;
+      case C.STATUS_COMPLETED:
+        break;
+    }
+  }
+};
+
+
+
+/**
+* @augments SIP.Transactions
+* @class Invite Client Transaction
+* @param {SIP.RequestSender} request_sender
+* @param {SIP.OutgoingRequest} request
+* @param {SIP.Transport} transport
+*/
+var InviteClientTransaction = function(request_sender, request, transport) {
+  var via,
+    tr = this;
+
+  this.type = C.INVITE_CLIENT;
+  this.transport = transport;
+  this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000);
+  this.request_sender = request_sender;
+  this.request = request;
+
+  this.logger = request_sender.ua.getLogger('sip.transaction.ict', this.id);
+
+  via = buildViaHeader(request_sender, transport, this.id);
+  this.request.setHeader('via', via);
+
+  this.request_sender.ua.newTransaction(this);
+
+  // Add the cancel property to the request.
+  //Will be called from the request instance, not the transaction itself.
+  this.request.cancel = function(reason, extraHeaders) {
+    extraHeaders = (extraHeaders || []).slice();
+    var length = extraHeaders.length;
+    var extraHeadersString = null;
+    for (var idx = 0; idx < length; idx++) {
+      extraHeadersString = (extraHeadersString || '') + extraHeaders[idx].trim() + '\r\n';
+    }
+
+    tr.cancel_request(tr, reason, extraHeadersString);
+  };
+};
+InviteClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype);
+
+InviteClientTransaction.prototype.stateChanged = function(state) {
+  this.state = state;
+  this.emit('stateChanged');
+};
+
+InviteClientTransaction.prototype.send = function() {
+  var tr = this;
+  this.stateChanged(C.STATUS_CALLING);
+  this.B = SIP.Timers.setTimeout(tr.timer_B.bind(tr), SIP.Timers.TIMER_B);
+
+  if(!this.transport.send(this.request)) {
+    this.onTransportError();
+  }
+};
+
+InviteClientTransaction.prototype.onTransportError = function() {
+  this.logger.log('transport error occurred, deleting INVITE client transaction ' + this.id);
+  SIP.Timers.clearTimeout(this.B);
+  SIP.Timers.clearTimeout(this.D);
+  SIP.Timers.clearTimeout(this.M);
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.request_sender.ua.destroyTransaction(this);
+
+  if (this.state !== C.STATUS_ACCEPTED) {
+    this.request_sender.onTransportError();
+  }
+};
+
+// RFC 6026 7.2
+InviteClientTransaction.prototype.timer_M = function() {
+  this.logger.log('Timer M expired for INVITE client transaction ' + this.id);
+
+  if(this.state === C.STATUS_ACCEPTED) {
+    SIP.Timers.clearTimeout(this.B);
+    this.stateChanged(C.STATUS_TERMINATED);
+    this.request_sender.ua.destroyTransaction(this);
+  }
+};
+
+// RFC 3261 17.1.1
+InviteClientTransaction.prototype.timer_B = function() {
+  this.logger.log('Timer B expired for INVITE client transaction ' + this.id);
+  if(this.state === C.STATUS_CALLING) {
+    this.stateChanged(C.STATUS_TERMINATED);
+    this.request_sender.ua.destroyTransaction(this);
+    this.request_sender.onRequestTimeout();
+  }
+};
+
+InviteClientTransaction.prototype.timer_D = function() {
+  this.logger.log('Timer D expired for INVITE client transaction ' + this.id);
+  SIP.Timers.clearTimeout(this.B);
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.request_sender.ua.destroyTransaction(this);
+};
+
+InviteClientTransaction.prototype.sendACK = function(response) {
+  var tr = this;
+
+  this.ack = 'ACK ' + this.request.ruri + ' SIP/2.0\r\n';
+  this.ack += 'Via: ' + this.request.headers['Via'].toString() + '\r\n';
+
+  if(this.request.headers['Route']) {
+    this.ack += 'Route: ' + this.request.headers['Route'].toString() + '\r\n';
+  }
+
+  this.ack += 'To: ' + response.getHeader('to') + '\r\n';
+  this.ack += 'From: ' + this.request.headers['From'].toString() + '\r\n';
+  this.ack += 'Call-ID: ' + this.request.headers['Call-ID'].toString() + '\r\n';
+  this.ack += 'Content-Length: 0\r\n';
+  this.ack += 'CSeq: ' + this.request.headers['CSeq'].toString().split(' ')[0];
+  this.ack += ' ACK\r\n\r\n';
+
+  this.D = SIP.Timers.setTimeout(tr.timer_D.bind(tr), SIP.Timers.TIMER_D);
+
+  this.transport.send(this.ack);
+};
+
+InviteClientTransaction.prototype.cancel_request = function(tr, reason, extraHeaders) {
+  var request = tr.request;
+
+  this.cancel = SIP.C.CANCEL + ' ' + request.ruri + ' SIP/2.0\r\n';
+  this.cancel += 'Via: ' + request.headers['Via'].toString() + '\r\n';
+
+  if(this.request.headers['Route']) {
+    this.cancel += 'Route: ' + request.headers['Route'].toString() + '\r\n';
+  }
+
+  this.cancel += 'To: ' + request.headers['To'].toString() + '\r\n';
+  this.cancel += 'From: ' + request.headers['From'].toString() + '\r\n';
+  this.cancel += 'Call-ID: ' + request.headers['Call-ID'].toString() + '\r\n';
+  this.cancel += 'CSeq: ' + request.headers['CSeq'].toString().split(' ')[0] +
+  ' CANCEL\r\n';
+
+  if(reason) {
+    this.cancel += 'Reason: ' + reason + '\r\n';
+  }
+
+  if (extraHeaders) {
+    this.cancel += extraHeaders;
+  }
+
+  this.cancel += 'Content-Length: 0\r\n\r\n';
+
+  // Send only if a provisional response (>100) has been received.
+  if(this.state === C.STATUS_PROCEEDING) {
+    this.transport.send(this.cancel);
+  }
+};
+
+InviteClientTransaction.prototype.receiveResponse = function(response) {
+  var
+  tr = this,
+  status_code = response.status_code;
+
+  if(status_code >= 100 && status_code <= 199) {
+    switch(this.state) {
+      case C.STATUS_CALLING:
+        this.stateChanged(C.STATUS_PROCEEDING);
+        this.request_sender.receiveResponse(response);
+        if(this.cancel) {
+          this.transport.send(this.cancel);
+        }
+        break;
+      case C.STATUS_PROCEEDING:
+        this.request_sender.receiveResponse(response);
+        break;
+    }
+  } else if(status_code >= 200 && status_code <= 299) {
+    switch(this.state) {
+      case C.STATUS_CALLING:
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_ACCEPTED);
+        this.M = SIP.Timers.setTimeout(tr.timer_M.bind(tr), SIP.Timers.TIMER_M);
+        this.request_sender.receiveResponse(response);
+        break;
+      case C.STATUS_ACCEPTED:
+        this.request_sender.receiveResponse(response);
+        break;
+    }
+  } else if(status_code >= 300 && status_code <= 699) {
+    switch(this.state) {
+      case C.STATUS_CALLING:
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_COMPLETED);
+        this.sendACK(response);
+        this.request_sender.receiveResponse(response);
+        break;
+      case C.STATUS_COMPLETED:
+        this.sendACK(response);
+        break;
+    }
+  }
+};
+
+
+/**
+ * @augments SIP.Transactions
+ * @class ACK Client Transaction
+ * @param {SIP.RequestSender} request_sender
+ * @param {SIP.OutgoingRequest} request
+ * @param {SIP.Transport} transport
+ */
+var AckClientTransaction = function(request_sender, request, transport) {
+  var via;
+
+  this.transport = transport;
+  this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000);
+  this.request_sender = request_sender;
+  this.request = request;
+
+  this.logger = request_sender.ua.getLogger('sip.transaction.nict', this.id);
+
+  via = buildViaHeader(request_sender, transport, this.id);
+  this.request.setHeader('via', via);
+};
+AckClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype);
+
+AckClientTransaction.prototype.send = function() {
+  if(!this.transport.send(this.request)) {
+    this.onTransportError();
+  }
+};
+
+AckClientTransaction.prototype.onTransportError = function() {
+  this.logger.log('transport error occurred, for an ACK client transaction ' + this.id);
+  this.request_sender.onTransportError();
+};
+
+
+/**
+* @augments SIP.Transactions
+* @class Non Invite Server Transaction
+* @param {SIP.IncomingRequest} request
+* @param {SIP.UA} ua
+*/
+var NonInviteServerTransaction = function(request, ua) {
+  this.type = C.NON_INVITE_SERVER;
+  this.id = request.via_branch;
+  this.request = request;
+  this.transport = request.transport;
+  this.ua = ua;
+  this.last_response = '';
+  request.server_transaction = this;
+
+  this.logger = ua.getLogger('sip.transaction.nist', this.id);
+
+  this.state = C.STATUS_TRYING;
+
+  ua.newTransaction(this);
+};
+NonInviteServerTransaction.prototype = Object.create(SIP.EventEmitter.prototype);
+
+NonInviteServerTransaction.prototype.stateChanged = function(state) {
+  this.state = state;
+  this.emit('stateChanged');
+};
+
+NonInviteServerTransaction.prototype.timer_J = function() {
+  this.logger.log('Timer J expired for non-INVITE server transaction ' + this.id);
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.ua.destroyTransaction(this);
+};
+
+NonInviteServerTransaction.prototype.onTransportError = function() {
+  if (!this.transportError) {
+    this.transportError = true;
+
+    this.logger.log('transport error occurred, deleting non-INVITE server transaction ' + this.id);
+
+    SIP.Timers.clearTimeout(this.J);
+    this.stateChanged(C.STATUS_TERMINATED);
+    this.ua.destroyTransaction(this);
+  }
+};
+
+NonInviteServerTransaction.prototype.receiveResponse = function(status_code, response) {
+  var tr = this;
+  var deferred = SIP.Utils.defer();
+
+  if(status_code === 100) {
+    /* RFC 4320 4.1
+     * 'A SIP element MUST NOT
+     * send any provisional response with a
+     * Status-Code other than 100 to a non-INVITE request.'
+     */
+    switch(this.state) {
+      case C.STATUS_TRYING:
+        this.stateChanged(C.STATUS_PROCEEDING);
+        if(!this.transport.send(response))  {
+          this.onTransportError();
+        }
+        break;
+      case C.STATUS_PROCEEDING:
+        this.last_response = response;
+        if(!this.transport.send(response)) {
+          this.onTransportError();
+          deferred.reject();
+        } else {
+          deferred.resolve();
+        }
+        break;
+    }
+  } else if(status_code >= 200 && status_code <= 699) {
+    switch(this.state) {
+      case C.STATUS_TRYING:
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_COMPLETED);
+        this.last_response = response;
+        this.J = SIP.Timers.setTimeout(tr.timer_J.bind(tr), SIP.Timers.TIMER_J);
+        if(!this.transport.send(response)) {
+          this.onTransportError();
+          deferred.reject();
+        } else {
+          deferred.resolve();
+        }
+        break;
+      case C.STATUS_COMPLETED:
+        break;
+    }
+  }
+
+  return deferred.promise;
+};
+
+/**
+* @augments SIP.Transactions
+* @class Invite Server Transaction
+* @param {SIP.IncomingRequest} request
+* @param {SIP.UA} ua
+*/
+var InviteServerTransaction = function(request, ua) {
+  this.type = C.INVITE_SERVER;
+  this.id = request.via_branch;
+  this.request = request;
+  this.transport = request.transport;
+  this.ua = ua;
+  this.last_response = '';
+  request.server_transaction = this;
+
+  this.logger = ua.getLogger('sip.transaction.ist', this.id);
+
+  this.state = C.STATUS_PROCEEDING;
+
+  ua.newTransaction(this);
+
+  this.resendProvisionalTimer = null;
+
+  request.reply(100);
+};
+InviteServerTransaction.prototype = Object.create(SIP.EventEmitter.prototype);
+
+InviteServerTransaction.prototype.stateChanged = function(state) {
+  this.state = state;
+  this.emit('stateChanged');
+};
+
+InviteServerTransaction.prototype.timer_H = function() {
+  this.logger.log('Timer H expired for INVITE server transaction ' + this.id);
+
+  if(this.state === C.STATUS_COMPLETED) {
+    this.logger.warn('transactions', 'ACK for INVITE server transaction was never received, call will be terminated');
+  }
+
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.ua.destroyTransaction(this);
+};
+
+InviteServerTransaction.prototype.timer_I = function() {
+  this.stateChanged(C.STATUS_TERMINATED);
+  this.ua.destroyTransaction(this);
+};
+
+// RFC 6026 7.1
+InviteServerTransaction.prototype.timer_L = function() {
+  this.logger.log('Timer L expired for INVITE server transaction ' + this.id);
+
+  if(this.state === C.STATUS_ACCEPTED) {
+    this.stateChanged(C.STATUS_TERMINATED);
+    this.ua.destroyTransaction(this);
+  }
+};
+
+InviteServerTransaction.prototype.onTransportError = function() {
+  if (!this.transportError) {
+    this.transportError = true;
+
+    this.logger.log('transport error occurred, deleting INVITE server transaction ' + this.id);
+
+    if (this.resendProvisionalTimer !== null) {
+      SIP.Timers.clearInterval(this.resendProvisionalTimer);
+      this.resendProvisionalTimer = null;
+    }
+
+    SIP.Timers.clearTimeout(this.L);
+    SIP.Timers.clearTimeout(this.H);
+    SIP.Timers.clearTimeout(this.I);
+
+    this.stateChanged(C.STATUS_TERMINATED);
+    this.ua.destroyTransaction(this);
+  }
+};
+
+InviteServerTransaction.prototype.resend_provisional = function() {
+  if(!this.transport.send(this.last_response)) {
+    this.onTransportError();
+  }
+};
+
+// INVITE Server Transaction RFC 3261 17.2.1
+InviteServerTransaction.prototype.receiveResponse = function(status_code, response) {
+  var tr = this;
+  var deferred = SIP.Utils.defer();
+
+  if(status_code >= 100 && status_code <= 199) {
+    switch(this.state) {
+      case C.STATUS_PROCEEDING:
+        if(!this.transport.send(response)) {
+          this.onTransportError();
+        }
+        this.last_response = response;
+        break;
+    }
+  }
+
+  if(status_code > 100 && status_code <= 199 && this.state === C.STATUS_PROCEEDING) {
+    // Trigger the resendProvisionalTimer only for the first non 100 provisional response.
+    if(this.resendProvisionalTimer === null) {
+      this.resendProvisionalTimer = SIP.Timers.setInterval(tr.resend_provisional.bind(tr),
+        SIP.Timers.PROVISIONAL_RESPONSE_INTERVAL);
+    }
+  } else if(status_code >= 200 && status_code <= 299) {
+    switch(this.state) {
+      case C.STATUS_PROCEEDING:
+        this.stateChanged(C.STATUS_ACCEPTED);
+        this.last_response = response;
+        this.L = SIP.Timers.setTimeout(tr.timer_L.bind(tr), SIP.Timers.TIMER_L);
+
+        if (this.resendProvisionalTimer !== null) {
+          SIP.Timers.clearInterval(this.resendProvisionalTimer);
+          this.resendProvisionalTimer = null;
+        }
+        /* falls through */
+        case C.STATUS_ACCEPTED:
+          // Note that this point will be reached for proceeding tr.state also.
+          if(!this.transport.send(response)) {
+            this.onTransportError();
+            deferred.reject();
+          } else {
+            deferred.resolve();
+          }
+          break;
+    }
+  } else if(status_code >= 300 && status_code <= 699) {
+    switch(this.state) {
+      case C.STATUS_PROCEEDING:
+        if (this.resendProvisionalTimer !== null) {
+          SIP.Timers.clearInterval(this.resendProvisionalTimer);
+          this.resendProvisionalTimer = null;
+        }
+
+        if(!this.transport.send(response)) {
+          this.onTransportError();
+          deferred.reject();
+        } else {
+          this.stateChanged(C.STATUS_COMPLETED);
+          this.H = SIP.Timers.setTimeout(tr.timer_H.bind(tr), SIP.Timers.TIMER_H);
+          deferred.resolve();
+        }
+        break;
+    }
+  }
+
+  return deferred.promise;
+};
+
+/**
+ * @function
+ * @param {SIP.UA} ua
+ * @param {SIP.IncomingRequest} request
+ *
+ * @return {boolean}
+ * INVITE:
+ *  _true_ if retransmission
+ *  _false_ new request
+ *
+ * ACK:
+ *  _true_  ACK to non2xx response
+ *  _false_ ACK must be passed to TU (accepted state)
+ *          ACK to 2xx response
+ *
+ * CANCEL:
+ *  _true_  no matching invite transaction
+ *  _false_ matching invite transaction and no final response sent
+ *
+ * OTHER:
+ *  _true_  retransmission
+ *  _false_ new request
+ */
+var checkTransaction = function(ua, request) {
+  var tr;
+
+  switch(request.method) {
+    case SIP.C.INVITE:
+      tr = ua.transactions.ist[request.via_branch];
+      if(tr) {
+        switch(tr.state) {
+          case C.STATUS_PROCEEDING:
+            tr.transport.send(tr.last_response);
+            break;
+
+            // RFC 6026 7.1 Invite retransmission
+            //received while in C.STATUS_ACCEPTED state. Absorb it.
+          case C.STATUS_ACCEPTED:
+            break;
+        }
+        return true;
+      }
+      break;
+    case SIP.C.ACK:
+      tr = ua.transactions.ist[request.via_branch];
+
+      // RFC 6026 7.1
+      if(tr) {
+        if(tr.state === C.STATUS_ACCEPTED) {
+          return false;
+        } else if(tr.state === C.STATUS_COMPLETED) {
+          tr.stateChanged(C.STATUS_CONFIRMED);
+          tr.I = SIP.Timers.setTimeout(tr.timer_I.bind(tr), SIP.Timers.TIMER_I);
+          return true;
+        }
+      }
+
+      // ACK to 2XX Response.
+      else {
+        return false;
+      }
+      break;
+    case SIP.C.CANCEL:
+      tr = ua.transactions.ist[request.via_branch];
+      if(tr) {
+        request.reply_sl(200);
+        if(tr.state === C.STATUS_PROCEEDING) {
+          return false;
+        } else {
+          return true;
+        }
+      } else {
+        request.reply_sl(481);
+        return true;
+      }
+      break;
+    default:
+
+      // Non-INVITE Server Transaction RFC 3261 17.2.2
+      tr = ua.transactions.nist[request.via_branch];
+      if(tr) {
+        switch(tr.state) {
+          case C.STATUS_TRYING:
+            break;
+          case C.STATUS_PROCEEDING:
+          case C.STATUS_COMPLETED:
+            tr.transport.send(tr.last_response);
+            break;
+        }
+        return true;
+      }
+      break;
+  }
+};
+
+SIP.Transactions = {
+  C: C,
+  checkTransaction: checkTransaction,
+  NonInviteClientTransaction: NonInviteClientTransaction,
+  InviteClientTransaction: InviteClientTransaction,
+  AckClientTransaction: AckClientTransaction,
+  NonInviteServerTransaction: NonInviteServerTransaction,
+  InviteServerTransaction: InviteServerTransaction
+};
+
+};
+
+},{}],28:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview Transport
+ */
+
+/**
+ * @augments SIP
+ * @class Transport
+ * @param {SIP.UA} ua
+ * @param {Object} server ws_server Object
+ */
+module.exports = function (SIP, WebSocket) {
+var Transport,
+  C = {
+    // Transport status codes
+    STATUS_READY:        0,
+    STATUS_DISCONNECTED: 1,
+    STATUS_ERROR:        2
+  };
+
+/**
+ * Compute an amount of time in seconds to wait before sending another
+ * keep-alive.
+ * @returns {Number}
+ */
+function computeKeepAliveTimeout(upperBound) {
+  var lowerBound = upperBound * 0.8;
+  return 1000 * (Math.random() * (upperBound - lowerBound) + lowerBound);
+}
+
+Transport = function(ua, server) {
+
+  this.logger = ua.getLogger('sip.transport');
+  this.ua = ua;
+  this.ws = null;
+  this.server = server;
+  this.reconnection_attempts = 0;
+  this.closed = false;
+  this.connected = false;
+  this.reconnectTimer = null;
+  this.lastTransportError = {};
+
+  this.keepAliveInterval = ua.configuration.keepAliveInterval;
+  this.keepAliveTimeout = null;
+  this.keepAliveTimer = null;
+
+  this.ua.transport = this;
+
+  // Connect
+  this.connect();
+};
+
+Transport.prototype = {
+  /**
+   * Send a message.
+   * @param {SIP.OutgoingRequest|String} msg
+   * @returns {Boolean}
+   */
+  send: function(msg) {
+    var message = msg.toString();
+
+    if(this.ws && this.ws.readyState === WebSocket.OPEN) {
+      if (this.ua.configuration.traceSip === true) {
+        this.logger.log('sending WebSocket message:\n\n' + message + '\n');
+      }
+      this.ws.send(message);
+      return true;
+    } else {
+      this.logger.warn('unable to send message, WebSocket is not open');
+      return false;
+    }
+  },
+
+  /**
+   * Send a keep-alive (a double-CRLF sequence).
+   * @private
+   * @returns {Boolean}
+   */
+  sendKeepAlive: function() {
+    if(this.keepAliveTimeout) { return; }
+
+    this.keepAliveTimeout = SIP.Timers.setTimeout(function() {
+      this.ua.emit('keepAliveTimeout');
+    }.bind(this), 10000);
+
+    return this.send('\r\n\r\n');
+  },
+
+  /**
+   * Start sending keep-alives.
+   * @private
+   */
+  startSendingKeepAlives: function() {
+    if (this.keepAliveInterval && !this.keepAliveTimer) {
+      this.keepAliveTimer = SIP.Timers.setTimeout(function() {
+        this.sendKeepAlive();
+        this.keepAliveTimer = null;
+        this.startSendingKeepAlives();
+      }.bind(this), computeKeepAliveTimeout(this.keepAliveInterval));
+    }
+  },
+
+  /**
+   * Stop sending keep-alives.
+   * @private
+   */
+  stopSendingKeepAlives: function() {
+    SIP.Timers.clearTimeout(this.keepAliveTimer);
+    SIP.Timers.clearTimeout(this.keepAliveTimeout);
+    this.keepAliveTimer = null;
+    this.keepAliveTimeout = null;
+  },
+
+  /**
+  * Disconnect socket.
+  */
+  disconnect: function() {
+    if(this.ws) {
+      // Clear reconnectTimer
+      SIP.Timers.clearTimeout(this.reconnectTimer);
+
+      this.stopSendingKeepAlives();
+
+      this.closed = true;
+      this.logger.log('closing WebSocket ' + this.server.ws_uri);
+      this.ws.close();
+      this.ws = null;
+    }
+
+    if (this.reconnectTimer !== null) {
+      SIP.Timers.clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+      this.ua.emit('disconnected', {
+        transport: this,
+        code: this.lastTransportError.code,
+        reason: this.lastTransportError.reason
+      });
+    }
+  },
+
+  /**
+  * Connect socket.
+  */
+  connect: function() {
+    var transport = this;
+
+    if(this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
+      this.logger.log('WebSocket ' + this.server.ws_uri + ' is already connected');
+      return false;
+    }
+
+    if(this.ws) {
+      this.ws.close();
+      this.ws = null;
+    }
+
+    this.logger.log('connecting to WebSocket ' + this.server.ws_uri);
+    this.ua.onTransportConnecting(this,
+      (this.reconnection_attempts === 0)?1:this.reconnection_attempts);
+
+    try {
+      this.ws = new WebSocket(this.server.ws_uri, 'sip');
+    } catch(e) {
+      this.logger.warn('error connecting to WebSocket ' + this.server.ws_uri + ': ' + e);
+    }
+
+    this.ws.binaryType = 'arraybuffer';
+
+    this.ws.onopen = function() {
+      transport.onOpen();
+    };
+
+    this.ws.onclose = function(e) {
+      transport.onClose(e);
+      // Always cleanup. Eases GC, prevents potential memory leaks.
+      this.onopen = null;
+      this.onclose = null;
+      this.onmessage = null;
+      this.onerror = null;
+    };
+
+    this.ws.onmessage = function(e) {
+      transport.onMessage(e);
+    };
+
+    this.ws.onerror = function(e) {
+      transport.onError(e);
+    };
+  },
+
+  // Transport Event Handlers
+
+  /**
+  * @event
+  * @param {event} e
+  */
+  onOpen: function() {
+    this.connected = true;
+
+    this.logger.log('WebSocket ' + this.server.ws_uri + ' connected');
+    // Clear reconnectTimer since we are not disconnected
+    if (this.reconnectTimer !== null) {
+      SIP.Timers.clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+    // Reset reconnection_attempts
+    this.reconnection_attempts = 0;
+    // Disable closed
+    this.closed = false;
+    // Trigger onTransportConnected callback
+    this.ua.onTransportConnected(this);
+    // Start sending keep-alives
+    this.startSendingKeepAlives();
+  },
+
+  /**
+  * @event
+  * @param {event} e
+  */
+  onClose: function(e) {
+    var connected_before = this.connected;
+
+    this.lastTransportError.code = e.code;
+    this.lastTransportError.reason = e.reason;
+
+    this.stopSendingKeepAlives();
+
+    if (this.reconnection_attempts > 0) {
+      this.logger.log('Reconnection attempt ' + this.reconnection_attempts + ' failed (code: ' + e.code + (e.reason? '| reason: ' + e.reason : '') +')');
+      this.reconnect();
+    } else {
+      this.connected = false;
+      this.logger.log('WebSocket disconnected (code: ' + e.code + (e.reason? '| reason: ' + e.reason : '') +')');
+
+      if(e.wasClean === false) {
+        this.logger.warn('WebSocket abrupt disconnection');
+      }
+      // Transport was connected
+      if(connected_before === true) {
+        this.ua.onTransportClosed(this);
+        // Check whether the user requested to close.
+        if(!this.closed) {
+          this.reconnect();
+        } else {
+          this.ua.emit('disconnected', {
+            transport: this,
+            code: this.lastTransportError.code,
+            reason: this.lastTransportError.reason
+          });
+
+        }
+      } else {
+        // This is the first connection attempt
+        //Network error
+        this.ua.onTransportError(this);
+      }
+    }
+  },
+
+  /**
+  * @event
+  * @param {event} e
+  */
+  onMessage: function(e) {
+    var message, transaction,
+      data = e.data;
+
+    // CRLF Keep Alive response from server. Ignore it.
+    if(data === '\r\n') {
+      SIP.Timers.clearTimeout(this.keepAliveTimeout);
+      this.keepAliveTimeout = null;
+
+      if (this.ua.configuration.traceSip === true) {
+        this.logger.log('received WebSocket message with CRLF Keep Alive response');
+      }
+
+      return;
+    }
+
+    // WebSocket binary message.
+    else if (typeof data !== 'string') {
+      try {
+        data = String.fromCharCode.apply(null, new Uint8Array(data));
+      } catch(evt) {
+        this.logger.warn('received WebSocket binary message failed to be converted into string, message discarded');
+        return;
+      }
+
+      if (this.ua.configuration.traceSip === true) {
+        this.logger.log('received WebSocket binary message:\n\n' + data + '\n');
+      }
+    }
+
+    // WebSocket text message.
+    else {
+      if (this.ua.configuration.traceSip === true) {
+        this.logger.log('received WebSocket text message:\n\n' + data + '\n');
+      }
+    }
+
+    message = SIP.Parser.parseMessage(data, this.ua);
+
+    if (!message) {
+      return;
+    }
+
+    if(this.ua.status === SIP.UA.C.STATUS_USER_CLOSED && message instanceof SIP.IncomingRequest) {
+      return;
+    }
+
+    // Do some sanity check
+    if(SIP.sanityCheck(message, this.ua, this)) {
+      if(message instanceof SIP.IncomingRequest) {
+        message.transport = this;
+        this.ua.receiveRequest(message);
+      } else if(message instanceof SIP.IncomingResponse) {
+        /* Unike stated in 18.1.2, if a response does not match
+        * any transaction, it is discarded here and no passed to the core
+        * in order to be discarded there.
+        */
+        switch(message.method) {
+          case SIP.C.INVITE:
+            transaction = this.ua.transactions.ict[message.via_branch];
+            if(transaction) {
+              transaction.receiveResponse(message);
+            }
+            break;
+          case SIP.C.ACK:
+            // Just in case ;-)
+            break;
+          default:
+            transaction = this.ua.transactions.nict[message.via_branch];
+            if(transaction) {
+              transaction.receiveResponse(message);
+            }
+            break;
+        }
+      }
+    }
+  },
+
+  /**
+  * @event
+  * @param {event} e
+  */
+  onError: function(e) {
+    this.logger.warn('WebSocket connection error: ' + JSON.stringify(e));
+  },
+
+  /**
+  * Reconnection attempt logic.
+  * @private
+  */
+  reconnect: function() {
+    var transport = this;
+
+    this.reconnection_attempts += 1;
+
+    if(this.reconnection_attempts > this.ua.configuration.wsServerMaxReconnection) {
+      this.logger.warn('maximum reconnection attempts for WebSocket ' + this.server.ws_uri);
+      this.ua.onTransportError(this);
+    } else if (this.reconnection_attempts === 1) {
+      this.logger.log('Connection to WebSocket ' + this.server.ws_uri + ' severed, attempting first reconnect');
+      transport.connect();
+    } else {
+      this.logger.log('trying to reconnect to WebSocket ' + this.server.ws_uri + ' (reconnection attempt ' + this.reconnection_attempts + ')');
+
+      this.reconnectTimer = SIP.Timers.setTimeout(function() {
+        transport.connect();
+        transport.reconnectTimer = null;
+      }, this.ua.configuration.wsServerReconnectionTimeout * 1000);
+    }
+  }
+};
+
+Transport.C = C;
+return Transport;
+};
+
+},{}],29:[function(require,module,exports){
+(function (global){
+"use strict";
+/**
+ * @augments SIP
+ * @class Class creating a SIP User Agent.
+ * @param {function returning SIP.MediaHandler} [configuration.mediaHandlerFactory]
+ *        A function will be invoked by each of the UA's Sessions to build the MediaHandler for that Session.
+ *        If no (or a falsy) value is provided, each Session will use a default (WebRTC) MediaHandler.
+ *
+ * @param {Object} [configuration.media] gets passed to SIP.MediaHandler.getDescription as mediaHint
+ */
+module.exports = function (SIP, environment) {
+var UA,
+  C = {
+    // UA status codes
+    STATUS_INIT:                0,
+    STATUS_STARTING:            1,
+    STATUS_READY:               2,
+    STATUS_USER_CLOSED:         3,
+    STATUS_NOT_READY:           4,
+
+    // UA error codes
+    CONFIGURATION_ERROR:  1,
+    NETWORK_ERROR:        2,
+
+    ALLOWED_METHODS: [
+      'ACK',
+      'CANCEL',
+      'INVITE',
+      'MESSAGE',
+      'BYE',
+      'OPTIONS',
+      'INFO',
+      'NOTIFY',
+      'REFER'
+    ],
+
+    ACCEPTED_BODY_TYPES: [
+      'application/sdp',
+      'application/dtmf-relay'
+    ],
+
+    MAX_FORWARDS: 70,
+    TAG_LENGTH: 10
+  };
+
+UA = function(configuration) {
+  var self = this;
+
+  // Helper function for forwarding events
+  function selfEmit(type) {
+    //registrationFailed handler is invoked with two arguments. Allow event handlers to be invoked with a variable no. of arguments
+    return self.emit.bind(self, type);
+  }
+
+  // Set Accepted Body Types
+  C.ACCEPTED_BODY_TYPES = C.ACCEPTED_BODY_TYPES.toString();
+
+  this.log = new SIP.LoggerFactory();
+  this.logger = this.getLogger('sip.ua');
+
+  this.cache = {
+    credentials: {}
+  };
+
+  this.configuration = {};
+  this.dialogs = {};
+
+  //User actions outside any session/dialog (MESSAGE)
+  this.applicants = {};
+
+  this.data = {};
+  this.sessions = {};
+  this.subscriptions = {};
+  this.earlySubscriptions = {};
+  this.transport = null;
+  this.contact = null;
+  this.status = C.STATUS_INIT;
+  this.error = null;
+  this.transactions = {
+    nist: {},
+    nict: {},
+    ist: {},
+    ict: {}
+  };
+
+  this.transportRecoverAttempts = 0;
+  this.transportRecoveryTimer = null;
+
+  Object.defineProperties(this, {
+    transactionsCount: {
+      get: function() {
+        var type,
+          transactions = ['nist','nict','ist','ict'],
+          count = 0;
+
+        for (type in transactions) {
+          count += Object.keys(this.transactions[transactions[type]]).length;
+        }
+
+        return count;
+      }
+    },
+
+    nictTransactionsCount: {
+      get: function() {
+        return Object.keys(this.transactions['nict']).length;
+      }
+    },
+
+    nistTransactionsCount: {
+      get: function() {
+        return Object.keys(this.transactions['nist']).length;
+      }
+    },
+
+    ictTransactionsCount: {
+      get: function() {
+        return Object.keys(this.transactions['ict']).length;
+      }
+    },
+
+    istTransactionsCount: {
+      get: function() {
+        return Object.keys(this.transactions['ist']).length;
+      }
+    }
+  });
+
+  /**
+   * Load configuration
+   *
+   * @throws {SIP.Exceptions.ConfigurationError}
+   * @throws {TypeError}
+   */
+
+  if(configuration === undefined) {
+    configuration = {};
+  } else if (typeof configuration === 'string' || configuration instanceof String) {
+    configuration = {
+      uri: configuration
+    };
+  }
+
+  // Apply log configuration if present
+  if (configuration.log) {
+    if (configuration.log.hasOwnProperty('builtinEnabled')) {
+      this.log.builtinEnabled = configuration.log.builtinEnabled;
+    }
+
+    if (configuration.log.hasOwnProperty('level')) {
+      this.log.level = configuration.log.level;
+    }
+
+    if (configuration.log.hasOwnProperty('connector')) {
+      this.log.connector = configuration.log.connector;
+    }
+  }
+
+  try {
+    this.loadConfig(configuration);
+  } catch(e) {
+    this.status = C.STATUS_NOT_READY;
+    this.error = C.CONFIGURATION_ERROR;
+    throw e;
+  }
+
+  // Initialize registerContext
+  this.registerContext = new SIP.RegisterContext(this);
+  this.registerContext.on('failed', selfEmit('registrationFailed'));
+  this.registerContext.on('registered', selfEmit('registered'));
+  this.registerContext.on('unregistered', selfEmit('unregistered'));
+
+  if(this.configuration.autostart) {
+    this.start();
+  }
+};
+UA.prototype = Object.create(SIP.EventEmitter.prototype);
+
+//=================
+//  High Level API
+//=================
+
+UA.prototype.register = function(options) {
+  this.configuration.register = true;
+  this.registerContext.register(options);
+
+  return this;
+};
+
+/**
+ * Unregister.
+ *
+ * @param {Boolean} [all] unregister all user bindings.
+ *
+ */
+UA.prototype.unregister = function(options) {
+  this.configuration.register = false;
+
+  var context = this.registerContext;
+  this.afterConnected(context.unregister.bind(context, options));
+
+  return this;
+};
+
+UA.prototype.isRegistered = function() {
+  return this.registerContext.registered;
+};
+
+/**
+ * Connection state.
+ * @param {Boolean}
+ */
+UA.prototype.isConnected = function() {
+  return this.transport ? this.transport.connected : false;
+};
+
+UA.prototype.afterConnected = function afterConnected (callback) {
+  if (this.isConnected()) {
+    callback();
+  } else {
+    this.once('connected', callback);
+  }
+};
+
+/**
+ * Make an outgoing call.
+ *
+ * @param {String} target
+ * @param {Object} views
+ * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint
+ *
+ * @throws {TypeError}
+ *
+ */
+UA.prototype.invite = function(target, options) {
+  var context = new SIP.InviteClientContext(this, target, options);
+
+  this.afterConnected(context.invite.bind(context));
+  this.emit('inviteSent', context);
+  return context;
+};
+
+UA.prototype.subscribe = function(target, event, options) {
+  var sub = new SIP.Subscription(this, target, event, options);
+
+  this.afterConnected(sub.subscribe.bind(sub));
+  return sub;
+};
+
+/**
+ * Send a message.
+ *
+ * @param {String} target
+ * @param {String} body
+ * @param {Object} [options]
+ *
+ * @throws {TypeError}
+ *
+ */
+UA.prototype.message = function(target, body, options) {
+  if (body === undefined) {
+    throw new TypeError('Not enough arguments');
+  }
+
+  // There is no Message module, so it is okay that the UA handles defaults here.
+  options = Object.create(options || Object.prototype);
+  options.contentType || (options.contentType = 'text/plain');
+  options.body = body;
+
+  return this.request(SIP.C.MESSAGE, target, options);
+};
+
+UA.prototype.request = function (method, target, options) {
+  var req = new SIP.ClientContext(this, method, target, options);
+
+  this.afterConnected(req.send.bind(req));
+  return req;
+};
+
+/**
+ * Gracefully close.
+ *
+ */
+UA.prototype.stop = function() {
+  var session, subscription, applicant,
+    ua = this;
+
+  function transactionsListener() {
+    if (ua.nistTransactionsCount === 0 && ua.nictTransactionsCount === 0) {
+        ua.removeListener('transactionDestroyed', transactionsListener);
+        ua.transport.disconnect();
+    }
+  }
+
+  this.logger.log('user requested closure...');
+
+  if(this.status === C.STATUS_USER_CLOSED) {
+    this.logger.warn('UA already closed');
+    return this;
+  }
+
+  // Clear transportRecoveryTimer
+  SIP.Timers.clearTimeout(this.transportRecoveryTimer);
+
+  // Close registerContext
+  this.logger.log('closing registerContext');
+  this.registerContext.close();
+
+  // Run  _terminate_ on every Session
+  for(session in this.sessions) {
+    this.logger.log('closing session ' + session);
+    this.sessions[session].terminate();
+  }
+
+  //Run _close_ on every confirmed Subscription
+  for(subscription in this.subscriptions) {
+    this.logger.log('unsubscribing from subscription ' + subscription);
+    this.subscriptions[subscription].close();
+  }
+
+  //Run _close_ on every early Subscription
+  for(subscription in this.earlySubscriptions) {
+    this.logger.log('unsubscribing from early subscription ' + subscription);
+    this.earlySubscriptions[subscription].close();
+  }
+
+  // Run  _close_ on every applicant
+  for(applicant in this.applicants) {
+    this.applicants[applicant].close();
+  }
+
+  this.status = C.STATUS_USER_CLOSED;
+
+  /*
+   * If the remaining transactions are all INVITE transactions, there is no need to
+   * wait anymore because every session has already been closed by this method.
+   * - locally originated sessions where terminated (CANCEL or BYE)
+   * - remotely originated sessions where rejected (4XX) or terminated (BYE)
+   * Remaining INVITE transactions belong tho sessions that where answered. This are in
+   * 'accepted' state due to timers 'L' and 'M' defined in [RFC 6026]
+   */
+  if (this.nistTransactionsCount === 0 && this.nictTransactionsCount === 0) {
+    this.transport.disconnect();
+  } else {
+    this.on('transactionDestroyed', transactionsListener);
+  }
+
+  if (typeof environment.removeEventListener === 'function') {
+    // Google Chrome Packaged Apps don't allow 'unload' listeners:
+    // unload is not available in packaged apps
+    if (!(global.chrome && global.chrome.app && global.chrome.app.runtime)) {
+      environment.removeEventListener('unload', this.environListener);
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Connect to the WS server if status = STATUS_INIT.
+ * Resume UA after being closed.
+ *
+ */
+UA.prototype.start = function() {
+  var server;
+
+  this.logger.log('user requested startup...');
+  if (this.status === C.STATUS_INIT) {
+    server = this.getNextWsServer();
+    this.status = C.STATUS_STARTING;
+    new SIP.Transport(this, server);
+  } else if(this.status === C.STATUS_USER_CLOSED) {
+    this.logger.log('resuming');
+    this.status = C.STATUS_READY;
+    this.transport.connect();
+  } else if (this.status === C.STATUS_STARTING) {
+    this.logger.log('UA is in STARTING status, not opening new connection');
+  } else if (this.status === C.STATUS_READY) {
+    this.logger.log('UA is in READY status, not resuming');
+  } else {
+    this.logger.error('Connection is down. Auto-Recovery system is trying to connect');
+  }
+
+  if (this.configuration.autostop && typeof environment.addEventListener === 'function') {
+    // Google Chrome Packaged Apps don't allow 'unload' listeners:
+    // unload is not available in packaged apps
+    if (!(global.chrome && global.chrome.app && global.chrome.app.runtime)) {
+      this.environListener = this.stop.bind(this);
+      environment.addEventListener('unload', this.environListener);
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Normalize a string into a valid SIP request URI
+ *
+ * @param {String} target
+ *
+ * @returns {SIP.URI|undefined}
+ */
+UA.prototype.normalizeTarget = function(target) {
+  return SIP.Utils.normalizeTarget(target, this.configuration.hostportParams);
+};
+
+
+//===============================
+//  Private (For internal use)
+//===============================
+
+UA.prototype.saveCredentials = function(credentials) {
+  this.cache.credentials[credentials.realm] = this.cache.credentials[credentials.realm] || {};
+  this.cache.credentials[credentials.realm][credentials.uri] = credentials;
+
+  return this;
+};
+
+UA.prototype.getCredentials = function(request) {
+  var realm, credentials;
+
+  realm = request.ruri.host;
+
+  if (this.cache.credentials[realm] && this.cache.credentials[realm][request.ruri]) {
+    credentials = this.cache.credentials[realm][request.ruri];
+    credentials.method = request.method;
+  }
+
+  return credentials;
+};
+
+UA.prototype.getLogger = function(category, label) {
+  return this.log.getLogger(category, label);
+};
+
+
+//==============================
+// Event Handlers
+//==============================
+
+/**
+ * Transport Close event
+ * @private
+ * @event
+ * @param {SIP.Transport} transport.
+ */
+UA.prototype.onTransportClosed = function(transport) {
+  // Run _onTransportError_ callback on every client transaction using _transport_
+  var type, idx, length,
+    client_transactions = ['nict', 'ict', 'nist', 'ist'];
+
+  transport.server.status = SIP.Transport.C.STATUS_DISCONNECTED;
+  this.logger.log('connection state set to '+ SIP.Transport.C.STATUS_DISCONNECTED);
+
+  length = client_transactions.length;
+  for (type = 0; type < length; type++) {
+    for(idx in this.transactions[client_transactions[type]]) {
+      this.transactions[client_transactions[type]][idx].onTransportError();
+    }
+  }
+
+  // Close sessions if GRUU is not being used
+  if (!this.contact.pub_gruu) {
+    this.closeSessionsOnTransportError();
+  }
+
+};
+
+/**
+ * Unrecoverable transport event.
+ * Connection reattempt logic has been done and didn't success.
+ * @private
+ * @event
+ * @param {SIP.Transport} transport.
+ */
+UA.prototype.onTransportError = function(transport) {
+  var server;
+
+  this.logger.log('transport ' + transport.server.ws_uri + ' failed | connection state set to '+ SIP.Transport.C.STATUS_ERROR);
+
+  // Close sessions.
+  //Mark this transport as 'down'
+  transport.server.status = SIP.Transport.C.STATUS_ERROR;
+
+  this.emit('disconnected', {
+    transport: transport
+  });
+
+  // try the next transport if the UA isn't closed
+  if(this.status === C.STATUS_USER_CLOSED) {
+    return;
+  }
+
+  server = this.getNextWsServer();
+
+  if(server) {
+    new SIP.Transport(this, server);
+  }else {
+    this.closeSessionsOnTransportError();
+    if (!this.error || this.error !== C.NETWORK_ERROR) {
+      this.status = C.STATUS_NOT_READY;
+      this.error = C.NETWORK_ERROR;
+    }
+    // Transport Recovery process
+    this.recoverTransport();
+  }
+};
+
+/**
+ * Transport connection event.
+ * @private
+ * @event
+ * @param {SIP.Transport} transport.
+ */
+UA.prototype.onTransportConnected = function(transport) {
+  this.transport = transport;
+
+  // Reset transport recovery counter
+  this.transportRecoverAttempts = 0;
+
+  transport.server.status = SIP.Transport.C.STATUS_READY;
+  this.logger.log('connection state set to '+ SIP.Transport.C.STATUS_READY);
+
+  if(this.status === C.STATUS_USER_CLOSED) {
+    return;
+  }
+
+  this.status = C.STATUS_READY;
+  this.error = null;
+
+  if(this.configuration.register) {
+    this.configuration.authenticationFactory.initialize().then(function () {
+      this.registerContext.onTransportConnected();
+    }.bind(this));
+  }
+
+  this.emit('connected', {
+    transport: transport
+  });
+};
+
+
+/**
+ * Transport connecting event
+ * @private
+ * @param {SIP.Transport} transport.
+ * #param {Integer} attempts.
+ */
+  UA.prototype.onTransportConnecting = function(transport, attempts) {
+    this.emit('connecting', {
+      transport: transport,
+      attempts: attempts
+    });
+  };
+
+
+/**
+ * new Transaction
+ * @private
+ * @param {SIP.Transaction} transaction.
+ */
+UA.prototype.newTransaction = function(transaction) {
+  this.transactions[transaction.type][transaction.id] = transaction;
+  this.emit('newTransaction', {transaction: transaction});
+};
+
+
+/**
+ * destroy Transaction
+ * @private
+ * @param {SIP.Transaction} transaction.
+ */
+UA.prototype.destroyTransaction = function(transaction) {
+  delete this.transactions[transaction.type][transaction.id];
+  this.emit('transactionDestroyed', {
+    transaction: transaction
+  });
+};
+
+
+//=========================
+// receiveRequest
+//=========================
+
+/**
+ * Request reception
+ * @private
+ * @param {SIP.IncomingRequest} request.
+ */
+UA.prototype.receiveRequest = function(request) {
+  var dialog, session, message, earlySubscription,
+    method = request.method,
+    transaction,
+    replaces,
+    replacedDialog,
+    self = this;
+
+  function ruriMatches (uri) {
+    return uri && uri.user === request.ruri.user;
+  }
+
+  // Check that request URI points to us
+  if(!(ruriMatches(this.configuration.uri) ||
+       ruriMatches(this.contact.uri) ||
+       ruriMatches(this.contact.pub_gruu) ||
+       ruriMatches(this.contact.temp_gruu))) {
+    this.logger.warn('Request-URI does not point to us');
+    if (request.method !== SIP.C.ACK) {
+      request.reply_sl(404);
+    }
+    return;
+  }
+
+  // Check request URI scheme
+  if(request.ruri.scheme === SIP.C.SIPS) {
+    request.reply_sl(416);
+    return;
+  }
+
+  // Check transaction
+  if(SIP.Transactions.checkTransaction(this, request)) {
+    return;
+  }
+
+  /* RFC3261 12.2.2
+   * Requests that do not change in any way the state of a dialog may be
+   * received within a dialog (for example, an OPTIONS request).
+   * They are processed as if they had been received outside the dialog.
+   */
+  if(method === SIP.C.OPTIONS) {
+    new SIP.Transactions.NonInviteServerTransaction(request, this);
+    request.reply(200, null, [
+      'Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString(),
+      'Accept: '+ C.ACCEPTED_BODY_TYPES
+    ]);
+  } else if (method === SIP.C.MESSAGE) {
+    message = new SIP.ServerContext(this, request);
+    message.body = request.body;
+    message.content_type = request.getHeader('Content-Type') || 'text/plain';
+
+    request.reply(200, null);
+    this.emit('message', message);
+  } else if (method !== SIP.C.INVITE &&
+             method !== SIP.C.ACK) {
+    // Let those methods pass through to normal processing for now.
+    transaction = new SIP.ServerContext(this, request);
+  }
+
+  // Initial Request
+  if(!request.to_tag) {
+    switch(method) {
+      case SIP.C.INVITE:
+        replaces =
+          this.configuration.replaces !== SIP.C.supported.UNSUPPORTED &&
+          request.parseHeader('replaces');
+
+        if (replaces) {
+          replacedDialog = this.dialogs[replaces.call_id + replaces.replaces_to_tag + replaces.replaces_from_tag];
+
+          if (!replacedDialog) {
+            //Replaced header without a matching dialog, reject
+            request.reply_sl(481, null);
+            return;
+          } else if (replacedDialog.owner.status === SIP.Session.C.STATUS_TERMINATED) {
+            request.reply_sl(603, null);
+            return;
+          } else if (replacedDialog.state === SIP.Dialog.C.STATUS_CONFIRMED && replaces.early_only) {
+            request.reply_sl(486, null);
+            return;
+          }
+        }
+
+        var isMediaSupported = this.configuration.mediaHandlerFactory.isSupported;
+        if(!isMediaSupported || isMediaSupported()) {
+          session = new SIP.InviteServerContext(this, request);
+          session.replacee = replacedDialog && replacedDialog.owner;
+          session.on('invite', function() {
+            self.emit('invite', this);
+          });
+        } else {
+          this.logger.warn('INVITE received but WebRTC is not supported');
+          request.reply(488);
+        }
+        break;
+      case SIP.C.BYE:
+        // Out of dialog BYE received
+        request.reply(481);
+        break;
+      case SIP.C.CANCEL:
+        session = this.findSession(request);
+        if(session) {
+          session.receiveRequest(request);
+        } else {
+          this.logger.warn('received CANCEL request for a non existent session');
+        }
+        break;
+      case SIP.C.ACK:
+        /* Absorb it.
+         * ACK request without a corresponding Invite Transaction
+         * and without To tag.
+         */
+        break;
+      case SIP.C.NOTIFY:
+        if (this.configuration.allowLegacyNotifications && this.listeners('notify').length > 0) {
+          request.reply(200, null);
+          self.emit('notify', {request: request});
+        } else {
+          request.reply(481, 'Subscription does not exist');
+        }
+        break;
+      default:
+        request.reply(405);
+        break;
+    }
+  }
+  // In-dialog request
+  else {
+    dialog = this.findDialog(request);
+
+    if(dialog) {
+      if (method === SIP.C.INVITE) {
+        new SIP.Transactions.InviteServerTransaction(request, this);
+      }
+      dialog.receiveRequest(request);
+    } else if (method === SIP.C.NOTIFY) {
+      session = this.findSession(request);
+      earlySubscription = this.findEarlySubscription(request);
+      if(session) {
+        session.receiveRequest(request);
+      } else if(earlySubscription) {
+        earlySubscription.receiveRequest(request);
+      } else {
+        this.logger.warn('received NOTIFY request for a non existent session or subscription');
+        request.reply(481, 'Subscription does not exist');
+      }
+    }
+    /* RFC3261 12.2.2
+     * Request with to tag, but no matching dialog found.
+     * Exception: ACK for an Invite request for which a dialog has not
+     * been created.
+     */
+    else {
+      if(method !== SIP.C.ACK) {
+        request.reply(481);
+      }
+    }
+  }
+};
+
+//=================
+// Utils
+//=================
+
+/**
+ * Get the session to which the request belongs to, if any.
+ * @private
+ * @param {SIP.IncomingRequest} request.
+ * @returns {SIP.OutgoingSession|SIP.IncomingSession|null}
+ */
+UA.prototype.findSession = function(request) {
+  return this.sessions[request.call_id + request.from_tag] ||
+          this.sessions[request.call_id + request.to_tag] ||
+          null;
+};
+
+/**
+ * Get the dialog to which the request belongs to, if any.
+ * @private
+ * @param {SIP.IncomingRequest}
+ * @returns {SIP.Dialog|null}
+ */
+UA.prototype.findDialog = function(request) {
+  return this.dialogs[request.call_id + request.from_tag + request.to_tag] ||
+          this.dialogs[request.call_id + request.to_tag + request.from_tag] ||
+          null;
+};
+
+/**
+ * Get the subscription which has not been confirmed to which the request belongs to, if any
+ * @private
+ * @param {SIP.IncomingRequest}
+ * @returns {SIP.Subscription|null}
+ */
+UA.prototype.findEarlySubscription = function(request) {
+  return this.earlySubscriptions[request.call_id + request.to_tag + request.getHeader('event')] || null;
+};
+
+/**
+ * Retrieve the next server to which connect.
+ * @private
+ * @returns {Object} ws_server
+ */
+UA.prototype.getNextWsServer = function() {
+  // Order servers by weight
+  var idx, length, ws_server,
+    candidates = [];
+
+  length = this.configuration.wsServers.length;
+  for (idx = 0; idx < length; idx++) {
+    ws_server = this.configuration.wsServers[idx];
+
+    if (ws_server.status === SIP.Transport.C.STATUS_ERROR) {
+      continue;
+    } else if (candidates.length === 0) {
+      candidates.push(ws_server);
+    } else if (ws_server.weight > candidates[0].weight) {
+      candidates = [ws_server];
+    } else if (ws_server.weight === candidates[0].weight) {
+      candidates.push(ws_server);
+    }
+  }
+
+  idx = Math.floor(Math.random() * candidates.length);
+
+  return candidates[idx];
+};
+
+/**
+ * Close all sessions on transport error.
+ * @private
+ */
+UA.prototype.closeSessionsOnTransportError = function() {
+  var idx;
+
+  // Run _transportError_ for every Session
+  for(idx in this.sessions) {
+    this.sessions[idx].onTransportError();
+  }
+  // Call registerContext _onTransportClosed_
+  this.registerContext.onTransportClosed();
+};
+
+UA.prototype.recoverTransport = function(ua) {
+  var idx, length, k, nextRetry, count, server;
+
+  ua = ua || this;
+  count = ua.transportRecoverAttempts;
+
+  length = ua.configuration.wsServers.length;
+  for (idx = 0; idx < length; idx++) {
+    ua.configuration.wsServers[idx].status = 0;
+  }
+
+  server = ua.getNextWsServer();
+
+  k = Math.floor((Math.random() * Math.pow(2,count)) +1);
+  nextRetry = k * ua.configuration.connectionRecoveryMinInterval;
+
+  if (nextRetry > ua.configuration.connectionRecoveryMaxInterval) {
+    this.logger.log('time for next connection attempt exceeds connectionRecoveryMaxInterval, resetting counter');
+    nextRetry = ua.configuration.connectionRecoveryMinInterval;
+    count = 0;
+  }
+
+  this.logger.log('next connection attempt in '+ nextRetry +' seconds');
+
+  this.transportRecoveryTimer = SIP.Timers.setTimeout(
+    function(){
+      ua.transportRecoverAttempts = count + 1;
+      new SIP.Transport(ua, server);
+    }, nextRetry * 1000);
+};
+
+function checkAuthenticationFactory (authenticationFactory) {
+  if (!(authenticationFactory instanceof Function)) {
+    return;
+  }
+  if (!authenticationFactory.initialize) {
+    authenticationFactory.initialize = function initialize () {
+      return SIP.Utils.Promise.resolve();
+    };
+  }
+  return authenticationFactory;
+}
+
+/**
+ * Configuration load.
+ * @private
+ * returns {Boolean}
+ */
+UA.prototype.loadConfig = function(configuration) {
+  // Settings and default values
+  var parameter, value, checked_value, hostportParams, registrarServer,
+    settings = {
+      /* Host address
+      * Value to be set in Via sent_by and host part of Contact FQDN
+      */
+      viaHost: SIP.Utils.createRandomToken(12) + '.invalid',
+
+      uri: new SIP.URI('sip', 'anonymous.' + SIP.Utils.createRandomToken(6), 'anonymous.invalid', null, null),
+      wsServers: [{
+        scheme: 'WSS',
+        sip_uri: '<sip:edge.sip.onsip.com;transport=ws;lr>',
+        status: 0,
+        weight: 0,
+        ws_uri: 'wss://edge.sip.onsip.com'
+      }],
+
+      //Custom Configuration Settings
+      custom: {},
+
+      //Display name
+      displayName: '',
+
+      // Password
+      password: null,
+
+      // Registration parameters
+      registerExpires: 600,
+      register: true,
+      registrarServer: null,
+
+      // Transport related parameters
+      wsServerMaxReconnection: 3,
+      wsServerReconnectionTimeout: 4,
+
+      connectionRecoveryMinInterval: 2,
+      connectionRecoveryMaxInterval: 30,
+
+      keepAliveInterval: 0,
+
+      extraSupported: [],
+
+      usePreloadedRoute: false,
+
+      //string to be inserted into User-Agent request header
+      userAgentString: SIP.C.USER_AGENT,
+
+      // Session parameters
+      iceCheckingTimeout: 5000,
+      noAnswerTimeout: 60,
+      stunServers: ['stun:stun.l.google.com:19302'],
+      turnServers: [],
+
+      // Logging parameters
+      traceSip: false,
+
+      // Hacks
+      hackViaTcp: false,
+      hackIpInContact: false,
+      hackWssInTransport: false,
+      hackAllowUnregisteredOptionTags: false,
+      hackCleanJitsiSdpImageattr: false,
+      hackStripTcp: false,
+
+      contactTransport: 'ws',
+      forceRport: false,
+
+      //autostarting
+      autostart: true,
+      autostop: true,
+
+      //Reliable Provisional Responses
+      rel100: SIP.C.supported.UNSUPPORTED,
+
+      // Replaces header (RFC 3891)
+      // http://tools.ietf.org/html/rfc3891
+      replaces: SIP.C.supported.UNSUPPORTED,
+
+      mediaHandlerFactory: SIP.WebRTC.MediaHandler.defaultFactory,
+
+      authenticationFactory: checkAuthenticationFactory(function authenticationFactory (ua) {
+        return new SIP.DigestAuthentication(ua);
+      }),
+
+      allowLegacyNotifications: false
+    };
+
+  // Pre-Configuration
+  function aliasUnderscored (parameter, logger) {
+    var underscored = parameter.replace(/([a-z][A-Z])/g, function (m) {
+      return m[0] + '_' + m[1].toLowerCase();
+    });
+
+    if (parameter === underscored) {
+      return;
+    }
+
+    var hasParameter = configuration.hasOwnProperty(parameter);
+    if (configuration.hasOwnProperty(underscored)) {
+      logger.warn(underscored + ' is deprecated, please use ' + parameter);
+      if (hasParameter) {
+        logger.warn(parameter + ' overriding ' + underscored);
+      }
+    }
+
+    configuration[parameter] = hasParameter ? configuration[parameter] : configuration[underscored];
+  }
+
+  var configCheck = this.getConfigurationCheck();
+
+  // Check Mandatory parameters
+  for(parameter in configCheck.mandatory) {
+    aliasUnderscored(parameter, this.logger);
+    if(!configuration.hasOwnProperty(parameter)) {
+      throw new SIP.Exceptions.ConfigurationError(parameter);
+    } else {
+      value = configuration[parameter];
+      checked_value = configCheck.mandatory[parameter](value);
+      if (checked_value !== undefined) {
+        settings[parameter] = checked_value;
+      } else {
+        throw new SIP.Exceptions.ConfigurationError(parameter, value);
+      }
+    }
+  }
+
+  SIP.Utils.optionsOverride(configuration, 'rel100', 'reliable', true, this.logger, SIP.C.supported.UNSUPPORTED);
+
+  var emptyArraysAllowed = ['stunServers', 'turnServers'];
+
+  // Check Optional parameters
+  for(parameter in configCheck.optional) {
+    aliasUnderscored(parameter, this.logger);
+    if(configuration.hasOwnProperty(parameter)) {
+      value = configuration[parameter];
+
+      // If the parameter value is an empty array, but shouldn't be, apply its default value.
+      if (value instanceof Array && value.length === 0 && emptyArraysAllowed.indexOf(parameter) < 0) { continue; }
+
+      // If the parameter value is null, empty string, or undefined then apply its default value.
+      if(value === null || value === "" || value === undefined) { continue; }
+      // If it's a number with NaN value then also apply its default value.
+      // NOTE: JS does not allow "value === NaN", the following does the work:
+      else if(typeof(value) === 'number' && isNaN(value)) { continue; }
+
+      checked_value = configCheck.optional[parameter](value);
+      if (checked_value !== undefined) {
+        settings[parameter] = checked_value;
+      } else {
+        throw new SIP.Exceptions.ConfigurationError(parameter, value);
+      }
+    }
+  }
+
+  // Sanity Checks
+
+  // Connection recovery intervals
+  if(settings.connectionRecoveryMaxInterval < settings.connectionRecoveryMinInterval) {
+    throw new SIP.Exceptions.ConfigurationError('connectionRecoveryMaxInterval', settings.connectionRecoveryMaxInterval);
+  }
+
+  // Post Configuration Process
+
+  // Allow passing 0 number as displayName.
+  if (settings.displayName === 0) {
+    settings.displayName = '0';
+  }
+
+  // Instance-id for GRUU
+  if (!settings.instanceId) {
+    settings.instanceId = SIP.Utils.newUUID();
+  }
+
+  // sipjsId instance parameter. Static random tag of length 5
+  settings.sipjsId = SIP.Utils.createRandomToken(5);
+
+  // String containing settings.uri without scheme and user.
+  hostportParams = settings.uri.clone();
+  hostportParams.user = null;
+  settings.hostportParams = hostportParams.toRaw().replace(/^sip:/i, '');
+
+  /* Check whether authorizationUser is explicitly defined.
+   * Take 'settings.uri.user' value if not.
+   */
+  if (!settings.authorizationUser) {
+    settings.authorizationUser = settings.uri.user;
+  }
+
+  /* If no 'registrarServer' is set use the 'uri' value without user portion. */
+  if (!settings.registrarServer) {
+    registrarServer = settings.uri.clone();
+    registrarServer.user = null;
+    settings.registrarServer = registrarServer;
+  }
+
+  // User noAnswerTimeout
+  settings.noAnswerTimeout = settings.noAnswerTimeout * 1000;
+
+  // Via Host
+  if (settings.hackIpInContact) {
+    if (typeof settings.hackIpInContact === 'boolean') {
+      settings.viaHost = SIP.Utils.getRandomTestNetIP();
+    }
+    else if (typeof settings.hackIpInContact === 'string') {
+      settings.viaHost = settings.hackIpInContact;
+    }
+  }
+
+  // Contact transport parameter
+  if (settings.hackWssInTransport) {
+    settings.contactTransport = 'wss';
+  }
+
+  this.contact = {
+    pub_gruu: null,
+    temp_gruu: null,
+    uri: new SIP.URI('sip', SIP.Utils.createRandomToken(8), settings.viaHost, null, {transport: settings.contactTransport}),
+    toString: function(options){
+      options = options || {};
+
+      var
+        anonymous = options.anonymous || null,
+        outbound = options.outbound || null,
+        contact = '<';
+
+      if (anonymous) {
+        contact += (this.temp_gruu || ('sip:anonymous@anonymous.invalid;transport='+settings.contactTransport)).toString();
+      } else {
+        contact += (this.pub_gruu || this.uri).toString();
+      }
+
+      if (outbound) {
+        contact += ';ob';
+      }
+
+      contact += '>';
+
+      return contact;
+    }
+  };
+
+  // media overrides mediaConstraints
+  SIP.Utils.optionsOverride(settings, 'media', 'mediaConstraints', true, this.logger);
+
+  var skeleton = {};
+  // Fill the value of the configuration_skeleton
+  for(parameter in settings) {
+    skeleton[parameter] = {
+      value: settings[parameter],
+      writable: (parameter === 'register' || parameter === 'custom'),
+      configurable: false
+    };
+  }
+
+  Object.defineProperties(this.configuration, skeleton);
+
+  this.logger.log('configuration parameters after validation:');
+  for(parameter in settings) {
+    switch(parameter) {
+      case 'uri':
+      case 'registrarServer':
+      case 'mediaHandlerFactory':
+        this.logger.log('· ' + parameter + ': ' + settings[parameter]);
+        break;
+      case 'password':
+        this.logger.log('· ' + parameter + ': ' + 'NOT SHOWN');
+        break;
+      default:
+        this.logger.log('· ' + parameter + ': ' + JSON.stringify(settings[parameter]));
+    }
+  }
+
+  return;
+};
+
+/**
+ * Configuration checker.
+ * @private
+ * @return {Boolean}
+ */
+UA.prototype.getConfigurationCheck = function () {
+  return {
+    mandatory: {
+    },
+
+    optional: {
+
+      uri: function(uri) {
+        var parsed;
+
+        if (!(/^sip:/i).test(uri)) {
+          uri = SIP.C.SIP + ':' + uri;
+        }
+        parsed = SIP.URI.parse(uri);
+
+        if(!parsed) {
+          return;
+        } else if(!parsed.user) {
+          return;
+        } else {
+          return parsed;
+        }
+      },
+
+      //Note: this function used to call 'this.logger.error' but calling 'this' with anything here is invalid
+      wsServers: function(wsServers) {
+        var idx, length, url;
+
+        /* Allow defining wsServers parameter as:
+         *  String: "host"
+         *  Array of Strings: ["host1", "host2"]
+         *  Array of Objects: [{ws_uri:"host1", weight:1}, {ws_uri:"host2", weight:0}]
+         *  Array of Objects and Strings: [{ws_uri:"host1"}, "host2"]
+         */
+        if (typeof wsServers === 'string') {
+          wsServers = [{ws_uri: wsServers}];
+        } else if (wsServers instanceof Array) {
+          length = wsServers.length;
+          for (idx = 0; idx < length; idx++) {
+            if (typeof wsServers[idx] === 'string'){
+              wsServers[idx] = {ws_uri: wsServers[idx]};
+            }
+          }
+        } else {
+          return;
+        }
+
+        if (wsServers.length === 0) {
+          return false;
+        }
+
+        length = wsServers.length;
+        for (idx = 0; idx < length; idx++) {
+          if (!wsServers[idx].ws_uri) {
+            return;
+          }
+          if (wsServers[idx].weight && !Number(wsServers[idx].weight)) {
+            return;
+          }
+
+          url = SIP.Grammar.parse(wsServers[idx].ws_uri, 'absoluteURI');
+
+          if(url === -1) {
+            return;
+          } else if(['wss', 'ws', 'udp'].indexOf(url.scheme) < 0) {
+            return;
+          } else {
+            wsServers[idx].sip_uri = '<sip:' + url.host + (url.port ? ':' + url.port : '') + ';transport=' + url.scheme.replace(/^wss$/i, 'ws') + ';lr>';
+
+            if (!wsServers[idx].weight) {
+              wsServers[idx].weight = 0;
+            }
+
+            wsServers[idx].status = 0;
+            wsServers[idx].scheme = url.scheme.toUpperCase();
+          }
+        }
+        return wsServers;
+      },
+
+      authorizationUser: function(authorizationUser) {
+        if(SIP.Grammar.parse('"'+ authorizationUser +'"', 'quoted_string') === -1) {
+          return;
+        } else {
+          return authorizationUser;
+        }
+      },
+
+      connectionRecoveryMaxInterval: function(connectionRecoveryMaxInterval) {
+        var value;
+        if(SIP.Utils.isDecimal(connectionRecoveryMaxInterval)) {
+          value = Number(connectionRecoveryMaxInterval);
+          if(value > 0) {
+            return value;
+          }
+        }
+      },
+
+      connectionRecoveryMinInterval: function(connectionRecoveryMinInterval) {
+        var value;
+        if(SIP.Utils.isDecimal(connectionRecoveryMinInterval)) {
+          value = Number(connectionRecoveryMinInterval);
+          if(value > 0) {
+            return value;
+          }
+        }
+      },
+
+      displayName: function(displayName) {
+        if(SIP.Grammar.parse('"' + displayName + '"', 'displayName') === -1) {
+          return;
+        } else {
+          return displayName;
+        }
+      },
+
+      hackViaTcp: function(hackViaTcp) {
+        if (typeof hackViaTcp === 'boolean') {
+          return hackViaTcp;
+        }
+      },
+
+      hackIpInContact: function(hackIpInContact) {
+        if (typeof hackIpInContact === 'boolean') {
+          return hackIpInContact;
+        }
+        else if (typeof hackIpInContact === 'string' && SIP.Grammar.parse(hackIpInContact, 'host') !== -1) {
+          return hackIpInContact;
+        }
+      },
+
+      iceCheckingTimeout: function(iceCheckingTimeout) {
+        if(SIP.Utils.isDecimal(iceCheckingTimeout)) {
+          return Math.max(500, iceCheckingTimeout);
+        }
+      },
+
+      hackWssInTransport: function(hackWssInTransport) {
+        if (typeof hackWssInTransport === 'boolean') {
+          return hackWssInTransport;
+        }
+      },
+
+      hackAllowUnregisteredOptionTags: function(hackAllowUnregisteredOptionTags) {
+        if (typeof hackAllowUnregisteredOptionTags === 'boolean') {
+          return hackAllowUnregisteredOptionTags;
+        }
+      },
+
+      hackCleanJitsiSdpImageattr: function(hackCleanJitsiSdpImageattr) {
+        if (typeof hackCleanJitsiSdpImageattr === 'boolean') {
+          return hackCleanJitsiSdpImageattr;
+        }
+      },
+
+      hackStripTcp: function(hackStripTcp) {
+        if (typeof hackStripTcp === 'boolean') {
+          return hackStripTcp;
+        }
+      },
+
+      contactTransport: function(contactTransport) {
+        if (typeof contactTransport === 'string') {
+          return contactTransport;
+        }
+      },
+
+      forceRport: function(forceRport) {
+        if (typeof forceRport === 'boolean') {
+          return forceRport;
+        }
+      },
+
+      instanceId: function(instanceId) {
+        if(typeof instanceId !== 'string') {
+          return;
+        }
+
+        if ((/^uuid:/i.test(instanceId))) {
+          instanceId = instanceId.substr(5);
+        }
+
+        if(SIP.Grammar.parse(instanceId, 'uuid') === -1) {
+          return;
+        } else {
+          return instanceId;
+        }
+      },
+
+      keepAliveInterval: function(keepAliveInterval) {
+        var value;
+        if (SIP.Utils.isDecimal(keepAliveInterval)) {
+          value = Number(keepAliveInterval);
+          if (value > 0) {
+            return value;
+          }
+        }
+      },
+
+      extraSupported: function(optionTags) {
+        var idx, length;
+
+        if (!(optionTags instanceof Array)) {
+          return;
+        }
+
+        length = optionTags.length;
+        for (idx = 0; idx < length; idx++) {
+          if (typeof optionTags[idx] !== 'string') {
+            return;
+          }
+        }
+
+        return optionTags;
+      },
+
+      noAnswerTimeout: function(noAnswerTimeout) {
+        var value;
+        if (SIP.Utils.isDecimal(noAnswerTimeout)) {
+          value = Number(noAnswerTimeout);
+          if (value > 0) {
+            return value;
+          }
+        }
+      },
+
+      password: function(password) {
+        return String(password);
+      },
+
+      rel100: function(rel100) {
+        if(rel100 === SIP.C.supported.REQUIRED) {
+          return SIP.C.supported.REQUIRED;
+        } else if (rel100 === SIP.C.supported.SUPPORTED) {
+          return SIP.C.supported.SUPPORTED;
+        } else  {
+          return SIP.C.supported.UNSUPPORTED;
+        }
+      },
+
+      replaces: function(replaces) {
+        if(replaces === SIP.C.supported.REQUIRED) {
+          return SIP.C.supported.REQUIRED;
+        } else if (replaces === SIP.C.supported.SUPPORTED) {
+          return SIP.C.supported.SUPPORTED;
+        } else  {
+          return SIP.C.supported.UNSUPPORTED;
+        }
+      },
+
+      register: function(register) {
+        if (typeof register === 'boolean') {
+          return register;
+        }
+      },
+
+      registerExpires: function(registerExpires) {
+        var value;
+        if (SIP.Utils.isDecimal(registerExpires)) {
+          value = Number(registerExpires);
+          if (value > 0) {
+            return value;
+          }
+        }
+      },
+
+      registrarServer: function(registrarServer) {
+        var parsed;
+
+        if(typeof registrarServer !== 'string') {
+          return;
+        }
+
+        if (!/^sip:/i.test(registrarServer)) {
+          registrarServer = SIP.C.SIP + ':' + registrarServer;
+        }
+        parsed = SIP.URI.parse(registrarServer);
+
+        if(!parsed) {
+          return;
+        } else if(parsed.user) {
+          return;
+        } else {
+          return parsed;
+        }
+      },
+
+      stunServers: function(stunServers) {
+        var idx, length, stun_server;
+
+        if (typeof stunServers === 'string') {
+          stunServers = [stunServers];
+        } else if (!(stunServers instanceof Array)) {
+          return;
+        }
+
+        length = stunServers.length;
+        for (idx = 0; idx < length; idx++) {
+          stun_server = stunServers[idx];
+          if (!(/^stuns?:/.test(stun_server))) {
+            stun_server = 'stun:' + stun_server;
+          }
+
+          if(SIP.Grammar.parse(stun_server, 'stun_URI') === -1) {
+            return;
+          } else {
+            stunServers[idx] = stun_server;
+          }
+        }
+        return stunServers;
+      },
+
+      traceSip: function(traceSip) {
+        if (typeof traceSip === 'boolean') {
+          return traceSip;
+        }
+      },
+
+      turnServers: function(turnServers) {
+        var idx, jdx, length, turn_server, num_turn_server_urls, url;
+
+        if (turnServers instanceof Array) {
+          // Do nothing
+        } else {
+          turnServers = [turnServers];
+        }
+
+        length = turnServers.length;
+        for (idx = 0; idx < length; idx++) {
+          turn_server = turnServers[idx];
+          //Backwards compatibility: Allow defining the turn_server url with the 'server' property.
+          if (turn_server.server) {
+            turn_server.urls = [turn_server.server];
+          }
+
+          if (!turn_server.urls) {
+            return;
+          }
+
+          if (turn_server.urls instanceof Array) {
+            num_turn_server_urls = turn_server.urls.length;
+          } else {
+            turn_server.urls = [turn_server.urls];
+            num_turn_server_urls = 1;
+          }
+
+          for (jdx = 0; jdx < num_turn_server_urls; jdx++) {
+            url = turn_server.urls[jdx];
+
+            if (!(/^turns?:/.test(url))) {
+              url = 'turn:' + url;
+            }
+
+            if(SIP.Grammar.parse(url, 'turn_URI') === -1) {
+              return;
+            }
+          }
+        }
+        return turnServers;
+      },
+
+      rtcpMuxPolicy: function(rtcpMuxPolicy) {
+        if (typeof rtcpMuxPolicy === 'string') {
+          return rtcpMuxPolicy;
+        }
+      },
+
+      userAgentString: function(userAgentString) {
+        if (typeof userAgentString === 'string') {
+          return userAgentString;
+        }
+      },
+
+      usePreloadedRoute: function(usePreloadedRoute) {
+        if (typeof usePreloadedRoute === 'boolean') {
+          return usePreloadedRoute;
+        }
+      },
+
+      wsServerMaxReconnection: function(wsServerMaxReconnection) {
+        var value;
+        if (SIP.Utils.isDecimal(wsServerMaxReconnection)) {
+          value = Number(wsServerMaxReconnection);
+          if (value > 0) {
+            return value;
+          }
+        }
+      },
+
+      wsServerReconnectionTimeout: function(wsServerReconnectionTimeout) {
+        var value;
+        if (SIP.Utils.isDecimal(wsServerReconnectionTimeout)) {
+          value = Number(wsServerReconnectionTimeout);
+          if (value > 0) {
+            return value;
+          }
+        }
+      },
+
+      autostart: function(autostart) {
+        if (typeof autostart === 'boolean') {
+          return autostart;
+        }
+      },
+
+      autostop: function(autostop) {
+        if (typeof autostop === 'boolean') {
+          return autostop;
+        }
+      },
+
+      mediaHandlerFactory: function(mediaHandlerFactory) {
+        if (mediaHandlerFactory instanceof Function) {
+          var promisifiedFactory = function promisifiedFactory () {
+            var mediaHandler = mediaHandlerFactory.apply(this, arguments);
+
+            function patchMethod (methodName) {
+              var method = mediaHandler[methodName];
+              if (method.length > 1) {
+                var callbacksFirst = methodName === 'getDescription';
+                mediaHandler[methodName] = SIP.Utils.promisify(mediaHandler, methodName, callbacksFirst);
+              }
+            }
+
+            patchMethod('getDescription');
+            patchMethod('setDescription');
+
+            return mediaHandler;
+          };
+
+          promisifiedFactory.isSupported = mediaHandlerFactory.isSupported;
+          return promisifiedFactory;
+        }
+      },
+
+      authenticationFactory: checkAuthenticationFactory,
+
+      allowLegacyNotifications: function(allowLegacyNotifications) {
+        if (typeof allowLegacyNotifications === 'boolean') {
+          return allowLegacyNotifications;
+        }
+      },
+
+      custom: function(custom) {
+        if (typeof custom === 'object') {
+          return custom;
+        }
+      }
+    }
+  };
+};
+
+UA.C = C;
+SIP.UA = UA;
+};
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],30:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview SIP URI
+ */
+
+/**
+ * @augments SIP
+ * @class Class creating a SIP URI.
+ *
+ * @param {String} [scheme]
+ * @param {String} [user]
+ * @param {String} host
+ * @param {String} [port]
+ * @param {Object} [parameters]
+ * @param {Object} [headers]
+ *
+ */
+module.exports = function (SIP) {
+var URI;
+
+URI = function(scheme, user, host, port, parameters, headers) {
+  var param, header, raw, normal;
+
+  // Checks
+  if(!host) {
+    throw new TypeError('missing or invalid "host" parameter');
+  }
+
+  // Initialize parameters
+  scheme = scheme || SIP.C.SIP;
+  this.parameters = {};
+  this.headers = {};
+
+  for (param in parameters) {
+    this.setParam(param, parameters[param]);
+  }
+
+  for (header in headers) {
+    this.setHeader(header, headers[header]);
+  }
+
+  // Raw URI
+  raw = {
+    scheme: scheme,
+    user: user,
+    host: host,
+    port: port
+  };
+
+  // Normalized URI
+  normal = {
+    scheme: scheme.toLowerCase(),
+    user: user,
+    host: host.toLowerCase(),
+    port: port
+  };
+
+  Object.defineProperties(this, {
+    _normal: {
+      get: function() { return normal; }
+    },
+
+    _raw: {
+      get: function() { return raw; }
+    },
+
+    scheme: {
+      get: function() { return normal.scheme; },
+      set: function(value) {
+        raw.scheme = value;
+        normal.scheme = value.toLowerCase();
+      }
+    },
+
+    user: {
+      get: function() { return normal.user; },
+      set: function(value) {
+        normal.user = raw.user = value;
+      }
+    },
+
+    host: {
+      get: function() { return normal.host; },
+      set: function(value) {
+        raw.host = value;
+        normal.host = value.toLowerCase();
+      }
+    },
+
+    aor: {
+      get: function() { return normal.user + '@' + normal.host; }
+    },
+
+    port: {
+      get: function() { return normal.port; },
+      set: function(value) {
+        normal.port = raw.port = value === 0 ? value : (parseInt(value,10) || null);
+      }
+    }
+  });
+};
+
+URI.prototype = {
+  setParam: function(key, value) {
+    if(key) {
+      this.parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString().toLowerCase();
+    }
+  },
+
+  getParam: function(key) {
+    if(key) {
+      return this.parameters[key.toLowerCase()];
+    }
+  },
+
+  hasParam: function(key) {
+    if(key) {
+      return (this.parameters.hasOwnProperty(key.toLowerCase()) && true) || false;
+    }
+  },
+
+  deleteParam: function(parameter) {
+    var value;
+    parameter = parameter.toLowerCase();
+    if (this.parameters.hasOwnProperty(parameter)) {
+      value = this.parameters[parameter];
+      delete this.parameters[parameter];
+      return value;
+    }
+  },
+
+  clearParams: function() {
+    this.parameters = {};
+  },
+
+  setHeader: function(name, value) {
+    this.headers[SIP.Utils.headerize(name)] = (value instanceof Array) ? value : [value];
+  },
+
+  getHeader: function(name) {
+    if(name) {
+      return this.headers[SIP.Utils.headerize(name)];
+    }
+  },
+
+  hasHeader: function(name) {
+    if(name) {
+      return (this.headers.hasOwnProperty(SIP.Utils.headerize(name)) && true) || false;
+    }
+  },
+
+  deleteHeader: function(header) {
+    var value;
+    header = SIP.Utils.headerize(header);
+    if(this.headers.hasOwnProperty(header)) {
+      value = this.headers[header];
+      delete this.headers[header];
+      return value;
+    }
+  },
+
+  clearHeaders: function() {
+    this.headers = {};
+  },
+
+  clone: function() {
+    return new URI(
+      this._raw.scheme,
+      this._raw.user,
+      this._raw.host,
+      this._raw.port,
+      JSON.parse(JSON.stringify(this.parameters)),
+      JSON.parse(JSON.stringify(this.headers)));
+  },
+
+  toRaw: function() {
+    return this._toString(this._raw);
+  },
+
+  toString: function() {
+    return this._toString(this._normal);
+  },
+
+  _toString: function(uri) {
+    var header, parameter, idx, uriString, headers = [];
+
+    uriString  = uri.scheme + ':';
+    // add slashes if it's not a sip(s) URI
+    if (!uri.scheme.toLowerCase().match("^sips?$")) {
+      uriString += "//";
+    }
+    if (uri.user) {
+      uriString += SIP.Utils.escapeUser(uri.user) + '@';
+    }
+    uriString += uri.host;
+    if (uri.port || uri.port === 0) {
+      uriString += ':' + uri.port;
+    }
+
+    for (parameter in this.parameters) {
+      uriString += ';' + parameter;
+
+      if (this.parameters[parameter] !== null) {
+        uriString += '='+ this.parameters[parameter];
+      }
+    }
+
+    for(header in this.headers) {
+      for(idx in this.headers[header]) {
+        headers.push(header + '=' + this.headers[header][idx]);
+      }
+    }
+
+    if (headers.length > 0) {
+      uriString += '?' + headers.join('&');
+    }
+
+    return uriString;
+  }
+};
+
+
+/**
+  * Parse the given string and returns a SIP.URI instance or undefined if
+  * it is an invalid URI.
+  * @public
+  * @param {String} uri
+  */
+URI.parse = function(uri) {
+  uri = SIP.Grammar.parse(uri,'SIP_URI');
+
+  if (uri !== -1) {
+    return uri;
+  } else {
+    return undefined;
+  }
+};
+
+SIP.URI = URI;
+};
+
+},{}],31:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview Utils
+ */
+
+module.exports = function (SIP, environment) {
+var Utils;
+
+Utils= {
+
+  Promise: environment.Promise,
+
+  defer: function defer () {
+    var deferred = {};
+    deferred.promise = new Utils.Promise(function (resolve, reject) {
+      deferred.resolve = resolve;
+      deferred.reject = reject;
+    });
+    return deferred;
+  },
+
+  promisify: function promisify (object, methodName, callbacksFirst) {
+    var oldMethod = object[methodName];
+    return function promisifiedMethod (arg, onSuccess, onFailure) {
+      return new Utils.Promise(function (resolve, reject) {
+        var oldArgs = [arg, resolve, reject];
+        if (callbacksFirst) {
+          oldArgs = [resolve, reject, arg];
+        }
+        oldMethod.apply(object, oldArgs);
+      }).then(onSuccess, onFailure);
+    };
+  },
+
+  augment: function (object, constructor, args, override) {
+    var idx, proto;
+
+    // Add public properties from constructor's prototype onto object
+    proto = constructor.prototype;
+    for (idx in proto) {
+      if (override || object[idx] === undefined) {
+        object[idx] = proto[idx];
+      }
+    }
+
+    // Construct the object as though it were just created by constructor
+    constructor.apply(object, args);
+  },
+
+  optionsOverride: function (options, winner, loser, isDeprecated, logger, defaultValue) {
+    if (isDeprecated && options[loser]) {
+      logger.warn(loser + ' is deprecated, please use ' + winner + ' instead');
+    }
+
+    if (options[winner] && options[loser]) {
+      logger.warn(winner + ' overriding ' + loser);
+    }
+
+    options[winner] = options[winner] || options[loser] || defaultValue;
+  },
+
+  str_utf8_length: function(string) {
+    return encodeURIComponent(string).replace(/%[A-F\d]{2}/g, 'U').length;
+  },
+
+  generateFakeSDP: function(body) {
+    if (!body) {
+      return;
+    }
+
+    var start = body.indexOf('o=');
+    var end = body.indexOf('\r\n', start);
+
+    return 'v=0\r\n' + body.slice(start, end) + '\r\ns=-\r\nt=0 0\r\nc=IN IP4 0.0.0.0';
+  },
+
+  isFunction: function(fn) {
+    if (fn !== undefined) {
+      return Object.prototype.toString.call(fn) === '[object Function]';
+    } else {
+      return false;
+    }
+  },
+
+  isDecimal: function (num) {
+    return !isNaN(num) && (parseFloat(num) === parseInt(num,10));
+  },
+
+  createRandomToken: function(size, base) {
+    var i, r,
+      token = '';
+
+    base = base || 32;
+
+    for( i=0; i < size; i++ ) {
+      r = Math.random() * base|0;
+      token += r.toString(base);
+    }
+
+    return token;
+  },
+
+  newTag: function() {
+    return SIP.Utils.createRandomToken(SIP.UA.C.TAG_LENGTH);
+  },
+
+  // http://stackoverflow.com/users/109538/broofa
+  newUUID: function() {
+    var UUID =  'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+      var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
+      return v.toString(16);
+    });
+
+    return UUID;
+  },
+
+  hostType: function(host) {
+    if (!host) {
+      return;
+    } else {
+      host = SIP.Grammar.parse(host,'host');
+      if (host !== -1) {
+        return host.host_type;
+      }
+    }
+  },
+
+  /**
+  * Normalize SIP URI.
+  * NOTE: It does not allow a SIP URI without username.
+  * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
+  * Detects the domain part (if given) and properly hex-escapes the user portion.
+  * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
+  * @private
+  * @param {String} target
+  * @param {String} [domain]
+  */
+  normalizeTarget: function(target, domain) {
+    var uri, target_array, target_user, target_domain;
+
+    // If no target is given then raise an error.
+    if (!target) {
+      return;
+    // If a SIP.URI instance is given then return it.
+    } else if (target instanceof SIP.URI) {
+      return target;
+
+    // If a string is given split it by '@':
+    // - Last fragment is the desired domain.
+    // - Otherwise append the given domain argument.
+    } else if (typeof target === 'string') {
+      target_array = target.split('@');
+
+      switch(target_array.length) {
+        case 1:
+          if (!domain) {
+            return;
+          }
+          target_user = target;
+          target_domain = domain;
+          break;
+        case 2:
+          target_user = target_array[0];
+          target_domain = target_array[1];
+          break;
+        default:
+          target_user = target_array.slice(0, target_array.length-1).join('@');
+          target_domain = target_array[target_array.length-1];
+      }
+
+      // Remove the URI scheme (if present).
+      target_user = target_user.replace(/^(sips?|tel):/i, '');
+
+      // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
+      if (/^[\-\.\(\)]*\+?[0-9\-\.\(\)]+$/.test(target_user)) {
+        target_user = target_user.replace(/[\-\.\(\)]/g, '');
+      }
+
+      // Build the complete SIP URI.
+      target = SIP.C.SIP + ':' + SIP.Utils.escapeUser(target_user) + '@' + target_domain;
+
+      // Finally parse the resulting URI.
+      if (uri = SIP.URI.parse(target)) {
+        return uri;
+      } else {
+        return;
+      }
+    } else {
+      return;
+    }
+  },
+
+  /**
+  * Hex-escape a SIP URI user.
+  * @private
+  * @param {String} user
+  */
+  escapeUser: function(user) {
+    // Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
+    return encodeURIComponent(decodeURIComponent(user)).replace(/%3A/ig, ':').replace(/%2B/ig, '+').replace(/%3F/ig, '?').replace(/%2F/ig, '/');
+  },
+
+  headerize: function(string) {
+    var exceptions = {
+      'Call-Id': 'Call-ID',
+      'Cseq': 'CSeq',
+      'Min-Se': 'Min-SE',
+      'Rack': 'RAck',
+      'Rseq': 'RSeq',
+      'Www-Authenticate': 'WWW-Authenticate'
+      },
+      name = string.toLowerCase().replace(/_/g,'-').split('-'),
+      hname = '',
+      parts = name.length, part;
+
+    for (part = 0; part < parts; part++) {
+      if (part !== 0) {
+        hname +='-';
+      }
+      hname += name[part].charAt(0).toUpperCase()+name[part].substring(1);
+    }
+    if (exceptions[hname]) {
+      hname = exceptions[hname];
+    }
+    return hname;
+  },
+
+  sipErrorCause: function(status_code) {
+    var cause;
+
+    for (cause in SIP.C.SIP_ERROR_CAUSES) {
+      if (SIP.C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1) {
+        return SIP.C.causes[cause];
+      }
+    }
+
+    return SIP.C.causes.SIP_FAILURE_CODE;
+  },
+
+  getReasonPhrase: function getReasonPhrase (code, specific) {
+    return specific || SIP.C.REASON_PHRASE[code] || '';
+  },
+
+  getReasonHeaderValue: function getReasonHeaderValue (code, reason) {
+    reason = SIP.Utils.getReasonPhrase(code, reason);
+    return 'SIP ;cause=' + code + ' ;text="' + reason + '"';
+  },
+
+  getCancelReason: function getCancelReason (code, reason) {
+    if (code && code < 200 || code > 699) {
+      throw new TypeError('Invalid status_code: ' + code);
+    } else if (code) {
+      return SIP.Utils.getReasonHeaderValue(code, reason);
+    }
+  },
+
+  buildStatusLine: function buildStatusLine (code, reason) {
+    code = code || null;
+    reason = reason || null;
+
+    // Validate code and reason values
+    if (!code || (code < 100 || code > 699)) {
+      throw new TypeError('Invalid status_code: '+ code);
+    } else if (reason && typeof reason !== 'string' && !(reason instanceof String)) {
+      throw new TypeError('Invalid reason_phrase: '+ reason);
+    }
+
+    reason = Utils.getReasonPhrase(code, reason);
+
+    return 'SIP/2.0 ' + code + ' ' + reason + '\r\n';
+  },
+
+  /**
+  * Generate a random Test-Net IP (http://tools.ietf.org/html/rfc5735)
+  * @private
+  */
+  getRandomTestNetIP: function() {
+    function getOctet(from,to) {
+      return Math.floor(Math.random()*(to-from+1)+from);
+    }
+    return '192.0.2.' + getOctet(1, 254);
+  },
+
+  // MD5 (Message-Digest Algorithm) http://www.webtoolkit.info
+  calculateMD5: function(string) {
+    function RotateLeft(lValue, iShiftBits) {
+      return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
+    }
+
+    function AddUnsigned(lX,lY) {
+      var lX4,lY4,lX8,lY8,lResult;
+      lX8 = (lX & 0x80000000);
+      lY8 = (lY & 0x80000000);
+      lX4 = (lX & 0x40000000);
+      lY4 = (lY & 0x40000000);
+      lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
+      if (lX4 & lY4) {
+        return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
+      }
+      if (lX4 | lY4) {
+        if (lResult & 0x40000000) {
+          return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
+        } else {
+          return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
+        }
+      } else {
+        return (lResult ^ lX8 ^ lY8);
+      }
+    }
+
+    function F(x,y,z) {
+      return (x & y) | ((~x) & z);
+    }
+
+    function G(x,y,z) {
+      return (x & z) | (y & (~z));
+    }
+
+    function H(x,y,z) {
+      return (x ^ y ^ z);
+    }
+
+    function I(x,y,z) {
+      return (y ^ (x | (~z)));
+    }
+
+    function FF(a,b,c,d,x,s,ac) {
+      a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
+      return AddUnsigned(RotateLeft(a, s), b);
+    }
+
+    function GG(a,b,c,d,x,s,ac) {
+      a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
+      return AddUnsigned(RotateLeft(a, s), b);
+    }
+
+    function HH(a,b,c,d,x,s,ac) {
+      a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
+      return AddUnsigned(RotateLeft(a, s), b);
+    }
+
+    function II(a,b,c,d,x,s,ac) {
+      a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
+      return AddUnsigned(RotateLeft(a, s), b);
+    }
+
+    function ConvertToWordArray(string) {
+      var lWordCount;
+      var lMessageLength = string.length;
+      var lNumberOfWords_temp1=lMessageLength + 8;
+      var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
+      var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
+      var lWordArray=Array(lNumberOfWords-1);
+      var lBytePosition = 0;
+      var lByteCount = 0;
+      while ( lByteCount < lMessageLength ) {
+        lWordCount = (lByteCount-(lByteCount % 4))/4;
+        lBytePosition = (lByteCount % 4)*8;
+        lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<<lBytePosition));
+        lByteCount++;
+      }
+      lWordCount = (lByteCount-(lByteCount % 4))/4;
+      lBytePosition = (lByteCount % 4)*8;
+      lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
+      lWordArray[lNumberOfWords-2] = lMessageLength<<3;
+      lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
+      return lWordArray;
+    }
+
+    function WordToHex(lValue) {
+      var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
+      for (lCount = 0;lCount<=3;lCount++) {
+        lByte = (lValue>>>(lCount*8)) & 255;
+        WordToHexValue_temp = "0" + lByte.toString(16);
+        WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
+      }
+      return WordToHexValue;
+    }
+
+    function Utf8Encode(string) {
+      string = string.replace(/\r\n/g,"\n");
+      var utftext = "";
+
+      for (var n = 0; n < string.length; n++) {
+        var c = string.charCodeAt(n);
+
+        if (c < 128) {
+          utftext += String.fromCharCode(c);
+        }
+        else if((c > 127) && (c < 2048)) {
+          utftext += String.fromCharCode((c >> 6) | 192);
+          utftext += String.fromCharCode((c & 63) | 128);
+        }
+        else {
+          utftext += String.fromCharCode((c >> 12) | 224);
+          utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+          utftext += String.fromCharCode((c & 63) | 128);
+        }
+      }
+      return utftext;
+    }
+
+    var x=[];
+    var k,AA,BB,CC,DD,a,b,c,d;
+    var S11=7, S12=12, S13=17, S14=22;
+    var S21=5, S22=9 , S23=14, S24=20;
+    var S31=4, S32=11, S33=16, S34=23;
+    var S41=6, S42=10, S43=15, S44=21;
+
+    string = Utf8Encode(string);
+
+    x = ConvertToWordArray(string);
+
+    a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
+
+    for (k=0;k<x.length;k+=16) {
+      AA=a; BB=b; CC=c; DD=d;
+      a=FF(a,b,c,d,x[k+0], S11,0xD76AA478);
+      d=FF(d,a,b,c,x[k+1], S12,0xE8C7B756);
+      c=FF(c,d,a,b,x[k+2], S13,0x242070DB);
+      b=FF(b,c,d,a,x[k+3], S14,0xC1BDCEEE);
+      a=FF(a,b,c,d,x[k+4], S11,0xF57C0FAF);
+      d=FF(d,a,b,c,x[k+5], S12,0x4787C62A);
+      c=FF(c,d,a,b,x[k+6], S13,0xA8304613);
+      b=FF(b,c,d,a,x[k+7], S14,0xFD469501);
+      a=FF(a,b,c,d,x[k+8], S11,0x698098D8);
+      d=FF(d,a,b,c,x[k+9], S12,0x8B44F7AF);
+      c=FF(c,d,a,b,x[k+10],S13,0xFFFF5BB1);
+      b=FF(b,c,d,a,x[k+11],S14,0x895CD7BE);
+      a=FF(a,b,c,d,x[k+12],S11,0x6B901122);
+      d=FF(d,a,b,c,x[k+13],S12,0xFD987193);
+      c=FF(c,d,a,b,x[k+14],S13,0xA679438E);
+      b=FF(b,c,d,a,x[k+15],S14,0x49B40821);
+      a=GG(a,b,c,d,x[k+1], S21,0xF61E2562);
+      d=GG(d,a,b,c,x[k+6], S22,0xC040B340);
+      c=GG(c,d,a,b,x[k+11],S23,0x265E5A51);
+      b=GG(b,c,d,a,x[k+0], S24,0xE9B6C7AA);
+      a=GG(a,b,c,d,x[k+5], S21,0xD62F105D);
+      d=GG(d,a,b,c,x[k+10],S22,0x2441453);
+      c=GG(c,d,a,b,x[k+15],S23,0xD8A1E681);
+      b=GG(b,c,d,a,x[k+4], S24,0xE7D3FBC8);
+      a=GG(a,b,c,d,x[k+9], S21,0x21E1CDE6);
+      d=GG(d,a,b,c,x[k+14],S22,0xC33707D6);
+      c=GG(c,d,a,b,x[k+3], S23,0xF4D50D87);
+      b=GG(b,c,d,a,x[k+8], S24,0x455A14ED);
+      a=GG(a,b,c,d,x[k+13],S21,0xA9E3E905);
+      d=GG(d,a,b,c,x[k+2], S22,0xFCEFA3F8);
+      c=GG(c,d,a,b,x[k+7], S23,0x676F02D9);
+      b=GG(b,c,d,a,x[k+12],S24,0x8D2A4C8A);
+      a=HH(a,b,c,d,x[k+5], S31,0xFFFA3942);
+      d=HH(d,a,b,c,x[k+8], S32,0x8771F681);
+      c=HH(c,d,a,b,x[k+11],S33,0x6D9D6122);
+      b=HH(b,c,d,a,x[k+14],S34,0xFDE5380C);
+      a=HH(a,b,c,d,x[k+1], S31,0xA4BEEA44);
+      d=HH(d,a,b,c,x[k+4], S32,0x4BDECFA9);
+      c=HH(c,d,a,b,x[k+7], S33,0xF6BB4B60);
+      b=HH(b,c,d,a,x[k+10],S34,0xBEBFBC70);
+      a=HH(a,b,c,d,x[k+13],S31,0x289B7EC6);
+      d=HH(d,a,b,c,x[k+0], S32,0xEAA127FA);
+      c=HH(c,d,a,b,x[k+3], S33,0xD4EF3085);
+      b=HH(b,c,d,a,x[k+6], S34,0x4881D05);
+      a=HH(a,b,c,d,x[k+9], S31,0xD9D4D039);
+      d=HH(d,a,b,c,x[k+12],S32,0xE6DB99E5);
+      c=HH(c,d,a,b,x[k+15],S33,0x1FA27CF8);
+      b=HH(b,c,d,a,x[k+2], S34,0xC4AC5665);
+      a=II(a,b,c,d,x[k+0], S41,0xF4292244);
+      d=II(d,a,b,c,x[k+7], S42,0x432AFF97);
+      c=II(c,d,a,b,x[k+14],S43,0xAB9423A7);
+      b=II(b,c,d,a,x[k+5], S44,0xFC93A039);
+      a=II(a,b,c,d,x[k+12],S41,0x655B59C3);
+      d=II(d,a,b,c,x[k+3], S42,0x8F0CCC92);
+      c=II(c,d,a,b,x[k+10],S43,0xFFEFF47D);
+      b=II(b,c,d,a,x[k+1], S44,0x85845DD1);
+      a=II(a,b,c,d,x[k+8], S41,0x6FA87E4F);
+      d=II(d,a,b,c,x[k+15],S42,0xFE2CE6E0);
+      c=II(c,d,a,b,x[k+6], S43,0xA3014314);
+      b=II(b,c,d,a,x[k+13],S44,0x4E0811A1);
+      a=II(a,b,c,d,x[k+4], S41,0xF7537E82);
+      d=II(d,a,b,c,x[k+11],S42,0xBD3AF235);
+      c=II(c,d,a,b,x[k+2], S43,0x2AD7D2BB);
+      b=II(b,c,d,a,x[k+9], S44,0xEB86D391);
+      a=AddUnsigned(a,AA);
+      b=AddUnsigned(b,BB);
+      c=AddUnsigned(c,CC);
+      d=AddUnsigned(d,DD);
+    }
+
+    var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d);
+
+    return temp.toLowerCase();
+  }
+};
+
+SIP.Utils = Utils;
+};
+
+},{}],32:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview WebRTC
+ */
+
+module.exports = function (SIP, environment) {
+var WebRTC;
+
+WebRTC = {};
+
+WebRTC.MediaHandler = require('./WebRTC/MediaHandler')(SIP);
+WebRTC.MediaStreamManager = require('./WebRTC/MediaStreamManager')(SIP, environment);
+
+var _isSupported;
+
+WebRTC.isSupported = function () {
+  if (_isSupported !== undefined) {
+    return _isSupported;
+  }
+
+  WebRTC.MediaStream = environment.MediaStream;
+  WebRTC.getUserMedia = environment.getUserMedia;
+  WebRTC.RTCPeerConnection = environment.RTCPeerConnection;
+  WebRTC.RTCSessionDescription = environment.RTCSessionDescription;
+
+  if (WebRTC.RTCPeerConnection && WebRTC.RTCSessionDescription) {
+    if (WebRTC.getUserMedia) {
+      WebRTC.getUserMedia = SIP.Utils.promisify(environment, 'getUserMedia');
+    }
+    _isSupported = true;
+  }
+  else {
+    _isSupported = false;
+  }
+  return _isSupported;
+};
+
+return WebRTC;
+};
+
+},{"./WebRTC/MediaHandler":33,"./WebRTC/MediaStreamManager":34}],33:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview MediaHandler
+ */
+
+/* MediaHandler
+ * @class PeerConnection helper Class.
+ * @param {SIP.Session} session
+ * @param {Object} [options]
+ * @param {SIP.WebRTC.MediaStreamManager} [options.mediaStreamManager]
+ *        The MediaStreamManager to acquire/release streams from/to.
+ *        If not provided, a default MediaStreamManager will be used.
+ */
+module.exports = function (SIP) {
+
+var MediaHandler = function(session, options) {
+  options = options || {};
+
+  this.logger = session.ua.getLogger('sip.invitecontext.mediahandler', session.id);
+  this.session = session;
+  this.localMedia = null;
+  this.ready = true;
+  this.mediaStreamManager = options.mediaStreamManager || new SIP.WebRTC.MediaStreamManager(this.logger);
+  this.audioMuted = false;
+  this.videoMuted = false;
+  this.local_hold = false;
+  this.remote_hold = false;
+
+  // old init() from here on
+  var servers = this.prepareIceServers(options.stunServers, options.turnServers);
+  this.RTCConstraints = options.RTCConstraints || {};
+
+  this.initPeerConnection(servers);
+
+  function selfEmit(mh, event) {
+    if (mh.mediaStreamManager.on) {
+      mh.mediaStreamManager.on(event, function () {
+        mh.emit.apply(mh, [event].concat(Array.prototype.slice.call(arguments)));
+      });
+    }
+  }
+
+  selfEmit(this, 'userMediaRequest');
+  selfEmit(this, 'userMedia');
+  selfEmit(this, 'userMediaFailed');
+};
+
+MediaHandler.defaultFactory = function defaultFactory (session, options) {
+  return new MediaHandler(session, options);
+};
+MediaHandler.defaultFactory.isSupported = function () {
+  return SIP.WebRTC.isSupported();
+};
+
+MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
+// Functions the session can use
+  isReady: {writable: true, value: function isReady () {
+    return this.ready;
+  }},
+
+  close: {writable: true, value: function close () {
+    this.logger.log('closing PeerConnection');
+    this._remoteStreams = [];
+    // have to check signalingState since this.close() gets called multiple times
+    // TODO figure out why that happens
+    if(this.peerConnection && this.peerConnection.signalingState !== 'closed') {
+      this.peerConnection.close();
+
+      if(this.localMedia) {
+        this.mediaStreamManager.release(this.localMedia);
+      }
+    }
+  }},
+
+  /**
+   * @param {SIP.WebRTC.MediaStream | (getUserMedia constraints)} [mediaHint]
+   *        the MediaStream (or the constraints describing it) to be used for the session
+   */
+  getDescription: {writable: true, value: function getDescription (mediaHint) {
+    var self = this;
+    var acquire = self.mediaStreamManager.acquire;
+    if (acquire.length > 1) {
+      acquire = SIP.Utils.promisify(this.mediaStreamManager, 'acquire', true);
+    }
+    mediaHint = mediaHint || {};
+    if (mediaHint.dataChannel === true) {
+      mediaHint.dataChannel = {};
+    }
+    this.mediaHint = mediaHint;
+
+    /*
+     * 1. acquire streams (skip if MediaStreams passed in)
+     * 2. addStreams
+     * 3. createOffer/createAnswer
+     */
+
+    var streamPromise;
+    if (self.localMedia) {
+      self.logger.log('already have local media');
+      streamPromise = SIP.Utils.Promise.resolve(self.localMedia);
+    }
+    else {
+      self.logger.log('acquiring local media');
+      streamPromise = acquire.call(self.mediaStreamManager, mediaHint)
+        .then(function acquireSucceeded(streams) {
+          self.logger.log('acquired local media streams');
+          self.localMedia = streams;
+          self.session.connecting();
+          return streams;
+        }, function acquireFailed(err) {
+          self.logger.error('unable to acquire streams');
+          self.logger.error(err);
+          self.session.connecting();
+          throw err;
+        })
+        .then(this.addStreams.bind(this))
+      ;
+    }
+
+    return streamPromise
+      .then(function streamAdditionSucceeded() {
+        if (self.hasOffer('remote')) {
+          self.peerConnection.ondatachannel = function (evt) {
+            self.dataChannel = evt.channel;
+            self.emit('dataChannel', self.dataChannel);
+          };
+        } else if (mediaHint.dataChannel &&
+                   self.peerConnection.createDataChannel) {
+          self.dataChannel = self.peerConnection.createDataChannel(
+            'sipjs',
+            mediaHint.dataChannel
+          );
+          self.emit('dataChannel', self.dataChannel);
+        }
+
+        self.render();
+        return self.createOfferOrAnswer(self.RTCConstraints);
+      })
+      .then(function(sdp) {
+        sdp = SIP.Hacks.Firefox.hasMissingCLineInSDP(sdp);
+
+        if (self.local_hold) {
+          // Don't receive media
+          // TODO - This will break for media streams with different directions.
+          if (!(/a=(sendrecv|sendonly|recvonly|inactive)/).test(sdp)) {
+            sdp = sdp.replace(/(m=[^\r]*\r\n)/g, '$1a=sendonly\r\n');
+          } else {
+            sdp = sdp.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n');
+            sdp = sdp.replace(/a=recvonly\r\n/g, 'a=inactive\r\n');
+          }
+        }
+
+        return {
+          body: sdp,
+          contentType: 'application/sdp'
+        };
+      })
+    ;
+  }},
+
+  /**
+   * Check if a SIP message contains a session description.
+   * @param {SIP.SIPMessage} message
+   * @returns {boolean}
+   */
+  hasDescription: {writeable: true, value: function hasDescription (message) {
+    return message.getHeader('Content-Type') === 'application/sdp' && !!message.body;
+  }},
+
+  /**
+   * Set the session description contained in a SIP message.
+   * @param {SIP.SIPMessage} message
+   * @returns {Promise}
+   */
+  setDescription: {writable: true, value: function setDescription (message) {
+    var self = this;
+    var sdp = message.body;
+
+    this.remote_hold = /a=(sendonly|inactive)/.test(sdp);
+
+    sdp = SIP.Hacks.Firefox.cannotHandleExtraWhitespace(sdp);
+    sdp = SIP.Hacks.AllBrowsers.maskDtls(sdp);
+
+    var rawDescription = {
+      type: this.hasOffer('local') ? 'answer' : 'offer',
+      sdp: sdp
+    };
+
+    this.emit('setDescription', rawDescription);
+
+    var description = new SIP.WebRTC.RTCSessionDescription(rawDescription);
+    return SIP.Utils.promisify(this.peerConnection, 'setRemoteDescription')(description)
+      .catch(function setRemoteDescriptionError(e) {
+        self.emit('peerConnection-setRemoteDescriptionFailed', e);
+        throw e;
+      });
+  }},
+
+  /**
+   * If the Session associated with this MediaHandler were to be referred,
+   * what mediaHint should be provided to the UA's invite method?
+   */
+  getReferMedia: {writable: true, value: function getReferMedia () {
+    function hasTracks (trackGetter, stream) {
+      return stream[trackGetter]().length > 0;
+    }
+
+    function bothHaveTracks (trackGetter) {
+      /* jshint validthis:true */
+      return this.getLocalStreams().some(hasTracks.bind(null, trackGetter)) &&
+             this.getRemoteStreams().some(hasTracks.bind(null, trackGetter));
+    }
+
+    return {
+      constraints: {
+        audio: bothHaveTracks.call(this, 'getAudioTracks'),
+        video: bothHaveTracks.call(this, 'getVideoTracks')
+      }
+    };
+  }},
+
+  updateIceServers: {writeable:true, value: function (options) {
+    var servers = this.prepareIceServers(options.stunServers, options.turnServers);
+    this.RTCConstraints = options.RTCConstraints || this.RTCConstraints;
+
+    this.initPeerConnection(servers);
+
+    /* once updateIce is implemented correctly, this is better than above
+    //no op if browser does not support this
+    if (!this.peerConnection.updateIce) {
+      return;
+    }
+
+    this.peerConnection.updateIce({'iceServers': servers}, this.RTCConstraints);
+    */
+  }},
+
+// Functions the session can use, but only because it's convenient for the application
+  isMuted: {writable: true, value: function isMuted () {
+    return {
+      audio: this.audioMuted,
+      video: this.videoMuted
+    };
+  }},
+
+  mute: {writable: true, value: function mute (options) {
+    if (this.getLocalStreams().length === 0) {
+      return;
+    }
+
+    options = options || {
+      audio: this.getLocalStreams()[0].getAudioTracks().length > 0,
+      video: this.getLocalStreams()[0].getVideoTracks().length > 0
+    };
+
+    var audioMuted = false,
+        videoMuted = false;
+
+    if (options.audio && !this.audioMuted) {
+      audioMuted = true;
+      this.audioMuted = true;
+      this.toggleMuteAudio(true);
+    }
+
+    if (options.video && !this.videoMuted) {
+      videoMuted = true;
+      this.videoMuted = true;
+      this.toggleMuteVideo(true);
+    }
+
+    //REVISIT
+    if (audioMuted || videoMuted) {
+      return {
+        audio: audioMuted,
+        video: videoMuted
+      };
+      /*this.session.onmute({
+        audio: audioMuted,
+        video: videoMuted
+      });*/
+    }
+  }},
+
+  unmute: {writable: true, value: function unmute (options) {
+    if (this.getLocalStreams().length === 0) {
+      return;
+    }
+
+    options = options || {
+      audio: this.getLocalStreams()[0].getAudioTracks().length > 0,
+      video: this.getLocalStreams()[0].getVideoTracks().length > 0
+    };
+
+    var audioUnMuted = false,
+        videoUnMuted = false;
+
+    if (options.audio && this.audioMuted) {
+      audioUnMuted = true;
+      this.audioMuted = false;
+      this.toggleMuteAudio(false);
+    }
+
+    if (options.video && this.videoMuted) {
+      videoUnMuted = true;
+      this.videoMuted = false;
+      this.toggleMuteVideo(false);
+    }
+
+    //REVISIT
+    if (audioUnMuted || videoUnMuted) {
+      return {
+        audio: audioUnMuted,
+        video: videoUnMuted
+      };
+      /*this.session.onunmute({
+        audio: audioUnMuted,
+        video: videoUnMuted
+      });*/
+    }
+  }},
+
+  hold: {writable: true, value: function hold () {
+    this.local_hold = true;
+    this.toggleMuteAudio(true);
+    this.toggleMuteVideo(true);
+  }},
+
+  unhold: {writable: true, value: function unhold () {
+    this.local_hold = false;
+
+    if (!this.audioMuted) {
+      this.toggleMuteAudio(false);
+    }
+
+    if (!this.videoMuted) {
+      this.toggleMuteVideo(false);
+    }
+  }},
+
+// Functions the application can use, but not the session
+  getLocalStreams: {writable: true, value: function getLocalStreams () {
+    var pc = this.peerConnection;
+    if (pc && pc.signalingState === 'closed') {
+      this.logger.warn('peerConnection is closed, getLocalStreams returning []');
+      return [];
+    }
+    return (pc.getLocalStreams && pc.getLocalStreams()) ||
+      pc.localStreams || [];
+  }},
+
+  getRemoteStreams: {writable: true, value: function getRemoteStreams () {
+    var pc = this.peerConnection;
+    if (pc && pc.signalingState === 'closed') {
+      this.logger.warn('peerConnection is closed, getRemoteStreams returning this._remoteStreams');
+      return this._remoteStreams;
+    }
+    return(pc.getRemoteStreams && pc.getRemoteStreams()) ||
+      pc.remoteStreams || [];
+  }},
+
+  render: {writable: true, value: function render (renderHint) {
+    renderHint = renderHint || (this.mediaHint && this.mediaHint.render);
+    if (!renderHint) {
+      return false;
+    }
+    var streamGetters = {
+      local: 'getLocalStreams',
+      remote: 'getRemoteStreams'
+    };
+    Object.keys(streamGetters).forEach(function (loc) {
+      var streamGetter = streamGetters[loc];
+      var streams = this[streamGetter]();
+      SIP.WebRTC.MediaStreamManager.render(streams, renderHint[loc]);
+    }.bind(this));
+  }},
+
+// Internal functions
+  hasOffer: {writable: true, value: function hasOffer (where) {
+    var offerState = 'have-' + where + '-offer';
+    return this.peerConnection.signalingState === offerState;
+    // TODO consider signalingStates with 'pranswer'?
+  }},
+
+  prepareIceServers: {writable: true, value: function prepareIceServers (stunServers, turnServers) {
+    var servers = [],
+      config = this.session.ua.configuration;
+
+    stunServers = stunServers || config.stunServers;
+    turnServers = turnServers || config.turnServers;
+
+    [].concat(stunServers).forEach(function (server) {
+      servers.push({'urls': server});
+    });
+
+    [].concat(turnServers).forEach(function (server) {
+      var turnServer = {'urls': server.urls};
+      if (server.username) {
+        turnServer.username = server.username;
+      }
+      if (server.password) {
+        turnServer.credential = server.password;
+      }
+      servers.push(turnServer);
+    });
+
+    return servers;
+  }},
+
+  initPeerConnection: {writable: true, value: function initPeerConnection(servers) {
+    var self = this,
+      config = this.session.ua.configuration;
+
+    this.onIceCompleted = SIP.Utils.defer();
+    this.onIceCompleted.promise.then(function(pc) {
+      self.emit('iceGatheringComplete', pc);
+      if (self.iceCheckingTimer) {
+        SIP.Timers.clearTimeout(self.iceCheckingTimer);
+        self.iceCheckingTimer = null;
+      }
+    });
+
+    if (this.peerConnection) {
+      this.peerConnection.close();
+    }
+
+    var connConfig = {
+      iceServers: servers
+    };
+
+    if (config.rtcpMuxPolicy) {
+      connConfig.rtcpMuxPolicy = config.rtcpMuxPolicy;
+    }
+
+    this.peerConnection = new SIP.WebRTC.RTCPeerConnection(connConfig);
+
+    // Firefox (35.0.1) sometimes throws on calls to peerConnection.getRemoteStreams
+    // even if peerConnection.onaddstream was just called. In order to make
+    // MediaHandler.prototype.getRemoteStreams work, keep track of them manually
+    this._remoteStreams = [];
+
+    this.peerConnection.onaddstream = function(e) {
+      self.logger.log('stream added: '+ e.stream.id);
+      self._remoteStreams.push(e.stream);
+      self.render();
+      self.emit('addStream', e);
+    };
+
+    this.peerConnection.onremovestream = function(e) {
+      self.logger.log('stream removed: '+ e.stream.id);
+    };
+
+    this.startIceCheckingTimer = function () {
+      if (!self.iceCheckingTimer) {
+        self.iceCheckingTimer = SIP.Timers.setTimeout(function() {
+          self.logger.log('RTCIceChecking Timeout Triggered after '+config.iceCheckingTimeout+' milliseconds');
+          self.onIceCompleted.resolve(this);
+        }.bind(this.peerConnection), config.iceCheckingTimeout);
+      }
+    };
+
+    this.peerConnection.onicecandidate = function(e) {
+      self.emit('iceCandidate', e);
+      if (e.candidate) {
+        self.logger.log('ICE candidate received: '+ (e.candidate.candidate === null ? null : e.candidate.candidate.trim()));
+        self.startIceCheckingTimer();
+      } else {
+        self.onIceCompleted.resolve(this);
+      }
+    };
+
+    this.peerConnection.onicegatheringstatechange = function () {
+      self.logger.log('RTCIceGatheringState changed: ' + this.iceGatheringState);
+      if (this.iceGatheringState === 'gathering') {
+        self.emit('iceGathering', this);
+      }
+      if (this.iceGatheringState === 'complete') {
+        self.onIceCompleted.resolve(this);
+      }
+    };
+
+    this.peerConnection.oniceconnectionstatechange = function() {  //need e for commented out case
+      var stateEvent;
+
+      if (this.iceConnectionState === 'checking') {
+        self.startIceCheckingTimer();
+      }
+
+      switch (this.iceConnectionState) {
+      case 'new':
+        stateEvent = 'iceConnection';
+        break;
+      case 'checking':
+        stateEvent = 'iceConnectionChecking';
+        break;
+      case 'connected':
+        stateEvent = 'iceConnectionConnected';
+        break;
+      case 'completed':
+        stateEvent = 'iceConnectionCompleted';
+        break;
+      case 'failed':
+        stateEvent = 'iceConnectionFailed';
+        break;
+      case 'disconnected':
+        stateEvent = 'iceConnectionDisconnected';
+        break;
+      case 'closed':
+        stateEvent = 'iceConnectionClosed';
+        break;
+      default:
+        self.logger.warn('Unknown iceConnection state:', this.iceConnectionState);
+        return;
+      }
+      self.emit(stateEvent, this);
+
+      //Bria state changes are always connected -> disconnected -> connected on accept, so session gets terminated
+      //normal calls switch from failed to connected in some cases, so checking for failed and terminated
+      /*if (this.iceConnectionState === 'failed') {
+        self.session.terminate({
+        cause: SIP.C.causes.RTP_TIMEOUT,
+        status_code: 200,
+        reason_phrase: SIP.C.causes.RTP_TIMEOUT
+      });
+      } else if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') {
+      self.onIceCompleted(this);
+      }*/
+    };
+
+    this.peerConnection.onstatechange = function() {
+      self.logger.log('PeerConnection state changed to "'+ this.readyState +'"');
+    };
+  }},
+
+  createOfferOrAnswer: {writable: true, value: function createOfferOrAnswer (constraints) {
+    var self = this;
+    var methodName;
+    var pc = self.peerConnection;
+
+    self.ready = false;
+    methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';
+
+    return SIP.Utils.promisify(pc, methodName, true)(constraints)
+      .catch(function methodError(e) {
+        self.emit('peerConnection-' + methodName + 'Failed', e);
+        throw e;
+      })
+      .then(SIP.Utils.promisify(pc, 'setLocalDescription'))
+      .catch(function localDescError(e) {
+        self.emit('peerConnection-selLocalDescriptionFailed', e);
+        throw e;
+      })
+      .then(function onSetLocalDescriptionSuccess() {
+        var deferred = SIP.Utils.defer();
+        if (pc.iceGatheringState === 'complete' && (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) {
+          deferred.resolve();
+        } else {
+          self.onIceCompleted.promise.then(deferred.resolve);
+        }
+        return deferred.promise;
+      })
+      .then(function readySuccess () {
+        var sdp = pc.localDescription.sdp;
+
+        sdp = SIP.Hacks.Chrome.needsExplicitlyInactiveSDP(sdp);
+        sdp = SIP.Hacks.AllBrowsers.unmaskDtls(sdp);
+
+        var sdpWrapper = {
+          type: methodName === 'createOffer' ? 'offer' : 'answer',
+          sdp: sdp
+        };
+
+        self.emit('getDescription', sdpWrapper);
+
+        if (self.session.ua.configuration.hackStripTcp) {
+          sdpWrapper.sdp = sdpWrapper.sdp.replace(/^a=candidate:\d+ \d+ tcp .*?\r\n/img, "");
+        }
+
+        self.ready = true;
+        return sdpWrapper.sdp;
+      })
+      .catch(function createOfferAnswerError (e) {
+        self.logger.error(e);
+        self.ready = true;
+        throw new SIP.Exceptions.GetDescriptionError(e);
+      })
+    ;
+  }},
+
+  addStreams: {writable: true, value: function addStreams (streams) {
+    try {
+      streams = [].concat(streams);
+      streams.forEach(function (stream) {
+        this.peerConnection.addStream(stream);
+      }, this);
+    } catch(e) {
+      this.logger.error('error adding stream');
+      this.logger.error(e);
+      return SIP.Utils.Promise.reject(e);
+    }
+
+    return SIP.Utils.Promise.resolve();
+  }},
+
+  toggleMuteHelper: {writable: true, value: function toggleMuteHelper (trackGetter, mute) {
+    this.getLocalStreams().forEach(function (stream) {
+      stream[trackGetter]().forEach(function (track) {
+        track.enabled = !mute;
+      });
+    });
+  }},
+
+  toggleMuteAudio: {writable: true, value: function toggleMuteAudio (mute) {
+    this.toggleMuteHelper('getAudioTracks', mute);
+  }},
+
+  toggleMuteVideo: {writable: true, value: function toggleMuteVideo (mute) {
+    this.toggleMuteHelper('getVideoTracks', mute);
+  }}
+});
+
+// Return since it will be assigned to a variable.
+return MediaHandler;
+};
+
+},{}],34:[function(require,module,exports){
+"use strict";
+/**
+ * @fileoverview MediaStreamManager
+ */
+
+/* MediaStreamManager
+ * @class Manages the acquisition and release of MediaStreams.
+ * @param {mediaHint} [defaultMediaHint] The mediaHint to use if none is provided to acquire()
+ */
+module.exports = function (SIP, environment) {
+
+// Default MediaStreamManager provides single-use streams created with getUserMedia
+var MediaStreamManager = function MediaStreamManager (logger, defaultMediaHint) {
+  if (!SIP.WebRTC.isSupported()) {
+    throw new SIP.Exceptions.NotSupportedError('Media not supported');
+  }
+
+  this.mediaHint = defaultMediaHint || {
+    constraints: {audio: true, video: true}
+  };
+
+  // map of streams to acquisition manner:
+  // true -> passed in as mediaHint.stream
+  // false -> getUserMedia
+  this.acquisitions = {};
+};
+MediaStreamManager.streamId = function (stream) {
+  return stream.getAudioTracks().concat(stream.getVideoTracks())
+    .map(function trackId (track) {
+      return track.id;
+    })
+    .join('');
+};
+
+/**
+ * @param {(Array of) MediaStream} streams - The streams to render
+ *
+ * @param {(Array of) HTMLMediaElement} elements
+ *        - The <audio>/<video> element(s) that should render the streams
+ *
+ * Each stream in streams renders to the corresponding element in elements,
+ * wrapping around elements if needed.
+ */
+MediaStreamManager.render = function render (streams, elements) {
+  if (!elements) {
+    return false;
+  }
+  if (Array.isArray(elements) && !elements.length) {
+    throw new TypeError('elements must not be empty');
+  }
+
+  function attachMediaStream(element, stream) {
+    element.srcObject = stream;
+  }
+
+  function ensureMediaPlaying (mediaElement) {
+    var interval = 100;
+    mediaElement.ensurePlayingIntervalId = SIP.Timers.setInterval(function () {
+      if (mediaElement.paused && mediaElement.srcObject) {
+        mediaElement.play();
+      }
+      else {
+        SIP.Timers.clearInterval(mediaElement.ensurePlayingIntervalId);
+      }
+    }, interval);
+  }
+
+  function attachAndPlay (elements, stream, index) {
+    var element = elements[index % elements.length];
+    if (typeof element === 'function') {
+      element = element();
+    }
+    (environment.attachMediaStream || attachMediaStream)(element, stream);
+    ensureMediaPlaying(element);
+  }
+
+  // [].concat "casts" `elements` into an array
+  // so forEach works even if `elements` was a single element
+  elements = [].concat(elements);
+  [].concat(streams).forEach(attachAndPlay.bind(null, elements));
+};
+
+MediaStreamManager.prototype = Object.create(SIP.EventEmitter.prototype, {
+  'acquire': {writable: true, value: function acquire (mediaHint) {
+    mediaHint = Object.keys(mediaHint || {}).length ? mediaHint : this.mediaHint;
+
+    var saveSuccess = function (isHintStream, streams) {
+      streams = [].concat(streams);
+      streams.forEach(function (stream) {
+        var streamId = MediaStreamManager.streamId(stream);
+        this.acquisitions[streamId] = !!isHintStream;
+      }, this);
+      return SIP.Utils.Promise.resolve(streams);
+    }.bind(this);
+
+    if (mediaHint.stream) {
+      return saveSuccess(true, mediaHint.stream);
+    } else {
+      // Fallback to audio/video enabled if no mediaHint can be found.
+      var constraints = mediaHint.constraints ||
+        (this.mediaHint && this.mediaHint.constraints) ||
+        {audio: true, video: true};
+
+      var deferred = SIP.Utils.defer();
+
+      /*
+       * Make the call asynchronous, so that ICCs have a chance
+       * to define callbacks to `userMediaRequest`
+       */
+      SIP.Timers.setTimeout(function () {
+        this.emit('userMediaRequest', constraints);
+
+        var emitThenCall = function (eventName, callback) {
+          var callbackArgs = Array.prototype.slice.call(arguments, 2);
+          // Emit with all of the arguments from the real callback.
+          var newArgs = [eventName].concat(callbackArgs);
+
+          this.emit.apply(this, newArgs);
+
+          return callback.apply(null, callbackArgs);
+        }.bind(this);
+
+        if (constraints.audio || constraints.video) {
+          deferred.resolve(
+            SIP.WebRTC.getUserMedia(constraints)
+            .then(
+              emitThenCall.bind(this, 'userMedia', saveSuccess.bind(null, false)),
+              emitThenCall.bind(this, 'userMediaFailed', function(e){throw e;})
+            )
+          );
+        } else {
+          // Local streams were explicitly excluded.
+          deferred.resolve([]);
+        }
+      }.bind(this), 0);
+
+      return deferred.promise;
+    }
+  }},
+
+  'release': {writable: true, value: function release (streams) {
+    streams = [].concat(streams);
+    streams.forEach(function (stream) {
+      var streamId = MediaStreamManager.streamId(stream);
+      if (this.acquisitions[streamId] === false) {
+        stream.getTracks().forEach(function (track) {
+          track.stop();
+        });
+      }
+      delete this.acquisitions[streamId];
+    }, this);
+  }},
+});
+
+// Return since it will be assigned to a variable.
+return MediaStreamManager;
+};
+
+},{}],35:[function(require,module,exports){
+(function (global){
+"use strict";
+
+var toplevel = global.window || global;
+
+function getPrefixedProperty (object, name) {
+  if (object == null) {
+    return;
+  }
+  var capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
+  var prefixedNames = [name, 'webkit' + capitalizedName, 'moz' + capitalizedName];
+  for (var i in prefixedNames) {
+    var property = object[prefixedNames[i]];
+    if (property) {
+      return property.bind(object);
+    }
+  }
+}
+
+module.exports = {
+  WebSocket: toplevel.WebSocket,
+  Transport: require('./Transport'),
+  open: toplevel.open,
+  Promise: toplevel.Promise,
+  timers: toplevel,
+
+  // Console is not defined in ECMAScript, so just in case...
+  console: toplevel.console || {
+    debug: function () {},
+    log: function () {},
+    warn: function () {},
+    error: function () {}
+  },
+
+  MediaStream: getPrefixedProperty(toplevel, 'MediaStream'),
+  getUserMedia: getPrefixedProperty(toplevel.navigator, 'getUserMedia'),
+  RTCPeerConnection: getPrefixedProperty(toplevel, 'RTCPeerConnection'),
+  RTCSessionDescription: getPrefixedProperty(toplevel, 'RTCSessionDescription'),
+
+  addEventListener: getPrefixedProperty(toplevel, 'addEventListener'),
+  removeEventListener: getPrefixedProperty(toplevel, 'removeEventListener'),
+  HTMLMediaElement: toplevel.HTMLMediaElement,
+
+  attachMediaStream: toplevel.attachMediaStream,
+  createObjectURL: toplevel.URL && toplevel.URL.createObjectURL,
+  revokeObjectURL: toplevel.URL && toplevel.URL.revokeObjectURL
+};
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./Transport":28}],36:[function(require,module,exports){
+"use strict";
+module.exports = require('./SIP')(require('./environment'));
+
+},{"./SIP":19,"./environment":35}]},{},[36])(36)
+});
\ No newline at end of file
diff --git a/bigbluebutton-html5/client/compatibility/verto-min.js b/bigbluebutton-html5/client/compatibility/verto-min.js
new file mode 100644
index 0000000000000000000000000000000000000000..d236a15d307c8a8defef5e007397deb5b32f69bf
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/verto-min.js
@@ -0,0 +1,561 @@
+
+(function($){function findLine(sdpLines,prefix,substr){return findLineInRange(sdpLines,0,-1,prefix,substr);}
+function findLineInRange(sdpLines,startLine,endLine,prefix,substr){var realEndLine=(endLine!=-1)?endLine:sdpLines.length;for(var i=startLine;i<realEndLine;++i){if(sdpLines[i].indexOf(prefix)===0){if(!substr||sdpLines[i].toLowerCase().indexOf(substr.toLowerCase())!==-1){return i;}}}
+return null;}
+function getCodecPayloadType(sdpLine){var pattern=new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');var result=sdpLine.match(pattern);return(result&&result.length==2)?result[1]:null;}
+function setDefaultCodec(mLine,payload){var elements=mLine.split(' ');var newLine=[];var index=0;for(var i=0;i<elements.length;i++){if(index===3){newLine[index++]=payload;}
+if(elements[i]!==payload)newLine[index++]=elements[i];}
+return newLine.join(' ');}
+$.FSRTC=function(options){this.options=$.extend({useVideo:null,useStereo:false,userData:null,localVideo:null,screenShare:false,useCamera:"any",iceServers:false,videoParams:{},audioParams:{},callbacks:{onICEComplete:function(){},onICE:function(){},onOfferSDP:function(){}},},options);this.audioEnabled=true;this.videoEnabled=true;this.mediaData={SDP:null,profile:{},candidateList:[]};this.constraints={offerToReceiveAudio:this.options.useSpeak==="none"?false:true,offerToReceiveVideo:this.options.useVideo?true:false,};if(self.options.useVideo){self.options.useVideo.style.display='none';}
+setCompat();checkCompat();};$.FSRTC.validRes=[];$.FSRTC.prototype.useVideo=function(obj,local){var self=this;if(obj){self.options.useVideo=obj;self.options.localVideo=local;self.constraints.offerToReceiveVideo=true;}else{self.options.useVideo=null;self.options.localVideo=null;self.constraints.offerToReceiveVideo=false;}
+if(self.options.useVideo){self.options.useVideo.style.display='none';}};$.FSRTC.prototype.useStereo=function(on){var self=this;self.options.useStereo=on;};$.FSRTC.prototype.stereoHack=function(sdp){var self=this;if(!self.options.useStereo){return sdp;}
+var sdpLines=sdp.split('\r\n');var opusIndex=findLine(sdpLines,'a=rtpmap','opus/48000'),opusPayload;if(!opusIndex){return sdp;}else{opusPayload=getCodecPayloadType(sdpLines[opusIndex]);}
+var fmtpLineIndex=findLine(sdpLines,'a=fmtp:'+opusPayload.toString());if(fmtpLineIndex===null){sdpLines[opusIndex]=sdpLines[opusIndex]+'\r\na=fmtp:'+opusPayload.toString()+" stereo=1; sprop-stereo=1"}else{sdpLines[fmtpLineIndex]=sdpLines[fmtpLineIndex].concat('; stereo=1; sprop-stereo=1');}
+sdp=sdpLines.join('\r\n');return sdp;};function setCompat(){}
+function checkCompat(){return true;}
+function onStreamError(self,e){console.log('There has been a problem retrieving the streams - did you allow access? Check Device Resolution',e);doCallback(self,"onError",e);}
+function onStreamSuccess(self,stream){console.log("Stream Success");doCallback(self,"onStream",stream);}
+function onICE(self,candidate){self.mediaData.candidate=candidate;self.mediaData.candidateList.push(self.mediaData.candidate);doCallback(self,"onICE");}
+function doCallback(self,func,arg){if(func in self.options.callbacks){self.options.callbacks[func](self,arg);}}
+function onICEComplete(self,candidate){console.log("ICE Complete");doCallback(self,"onICEComplete");}
+function onChannelError(self,e){console.error("Channel Error",e);doCallback(self,"onError",e);}
+function onICESDP(self,sdp){self.mediaData.SDP=self.stereoHack(sdp.sdp);console.log("ICE SDP");doCallback(self,"onICESDP");}
+function onAnswerSDP(self,sdp){self.answer.SDP=self.stereoHack(sdp.sdp);console.log("ICE ANSWER SDP");doCallback(self,"onAnswerSDP",self.answer.SDP);}
+function onMessage(self,msg){console.log("Message");doCallback(self,"onICESDP",msg);}
+FSRTCattachMediaStream=function(element,stream){if(typeof element.srcObject!=='undefined'){element.srcObject=stream;}else if(typeof element.src!=='undefined'){element.src=URL.createObjectURL(stream);}else{console.error('Error attaching stream to element.');}}
+function onRemoteStream(self,stream){if(self.options.useVideo){self.options.useVideo.style.display='block';}
+var element=self.options.useAudio;console.log("REMOTE STREAM",stream,element);FSRTCattachMediaStream(element,stream);self.options.useAudio.play();self.remoteStream=stream;}
+function onOfferSDP(self,sdp){self.mediaData.SDP=self.stereoHack(sdp.sdp);console.log("Offer SDP");doCallback(self,"onOfferSDP");}
+$.FSRTC.prototype.answer=function(sdp,onSuccess,onError){this.peer.addAnswerSDP({type:"answer",sdp:sdp},onSuccess,onError);};$.FSRTC.prototype.stopPeer=function(){if(self.peer){console.log("stopping peer");self.peer.stop();}}
+$.FSRTC.prototype.stop=function(){var self=this;if(self.options.useVideo){self.options.useVideo.style.display='none';self.options.useVideo['src']='';}
+if(self.localStream){if(typeof self.localStream.stop=='function'){self.localStream.stop();}else{if(self.localStream.active){var tracks=self.localStream.getTracks();console.log(tracks);tracks.forEach(function(track,index){console.log(track);track.stop();})}}
+self.localStream=null;}
+if(self.options.localVideo){self.options.localVideo.style.display='none';self.options.localVideo['src']='';}
+if(self.options.localVideoStream){if(typeof self.options.localVideoStream.stop=='function'){self.options.localVideoStream.stop();}else{if(self.options.localVideoStream.active){var tracks=self.options.localVideoStream.getTracks();console.log(tracks);tracks.forEach(function(track,index){console.log(track);track.stop();})}}}
+if(self.peer){console.log("stopping peer");self.peer.stop();}};$.FSRTC.prototype.getMute=function(){var self=this;return self.audioEnabled;}
+$.FSRTC.prototype.setMute=function(what){var self=this;var audioTracks=self.localStream.getAudioTracks();for(var i=0,len=audioTracks.length;i<len;i++){switch(what){case"on":audioTracks[i].enabled=true;break;case"off":audioTracks[i].enabled=false;break;case"toggle":audioTracks[i].enabled=!audioTracks[i].enabled;default:break;}
+self.audioEnabled=audioTracks[i].enabled;}
+return!self.audioEnabled;}
+$.FSRTC.prototype.getVideoMute=function(){var self=this;return self.videoEnabled;}
+$.FSRTC.prototype.setVideoMute=function(what){var self=this;var videoTracks=self.localStream.getVideoTracks();for(var i=0,len=videoTracks.length;i<len;i++){switch(what){case"on":videoTracks[i].enabled=true;break;case"off":videoTracks[i].enabled=false;break;case"toggle":videoTracks[i].enabled=!videoTracks[i].enabled;default:break;}
+self.videoEnabled=videoTracks[i].enabled;}
+return!self.videoEnabled;}
+$.FSRTC.prototype.createAnswer=function(params){var self=this;self.type="answer";self.remoteSDP=params.sdp;console.debug("inbound sdp: ",params.sdp);function onSuccess(stream){self.localStream=stream;self.peer=FSRTCPeerConnection({type:self.type,attachStream:self.localStream,onICE:function(candidate){return onICE(self,candidate);},onICEComplete:function(){return onICEComplete(self);},onRemoteStream:function(stream){return onRemoteStream(self,stream);},onICESDP:function(sdp){return onICESDP(self,sdp);},onChannelError:function(e){return onChannelError(self,e);},constraints:self.constraints,iceServers:self.options.iceServers,offerSDP:{type:"offer",sdp:self.remoteSDP}});onStreamSuccess(self,stream);}
+function onError(e){onStreamError(self,e);}
+var mediaParams=getMediaParams(self);console.log("Audio constraints",mediaParams.audio);console.log("Video constraints",mediaParams.video);if(self.options.useVideo&&self.options.localVideo){getUserMedia({constraints:{audio:false,video:{},},localVideo:self.options.localVideo,onsuccess:function(e){self.options.localVideoStream=e;console.log("local video ready");},onerror:function(e){console.error("local video error!");}});}
+getUserMedia({constraints:{audio:mediaParams.audio,video:mediaParams.video},video:mediaParams.useVideo,onsuccess:onSuccess,onerror:onError});};function getMediaParams(obj){var audio;if(obj.options.useMic&&obj.options.useMic==="none"){console.log("Microphone Disabled");audio=false;}else if(obj.options.videoParams&&obj.options.screenShare){console.error("SCREEN SHARE",obj.options.videoParams);audio=false;}else{audio={};if(obj.options.audioParams){audio=obj.options.audioParams;}
+if(obj.options.useMic!=="any"){audio.deviceId={exact:obj.options.useMic};}}
+if(obj.options.useVideo&&obj.options.localVideo){getUserMedia({constraints:{audio:false,video:obj.options.videoParams},localVideo:obj.options.localVideo,onsuccess:function(e){self.options.localVideoStream=e;console.log("local video ready");},onerror:function(e){console.error("local video error!");}});}
+var video={};var bestFrameRate=obj.options.videoParams.vertoBestFrameRate;var minFrameRate=obj.options.videoParams.minFrameRate||15;delete obj.options.videoParams.vertoBestFrameRate;if(obj.options.screenShare){if(!obj.options.useCamera&&!!navigator.mozGetUserMedia){var dowin=window.confirm("Do you want to share an application window?  If not you can share an entire screen.");video={width:{min:obj.options.videoParams.minWidth,max:obj.options.videoParams.maxWidth},height:{min:obj.options.videoParams.minHeight,max:obj.options.videoParams.maxHeight},mediaSource:dowin?"window":"screen"}}else{var opt=[];if(obj.options.useCamera){opt.push({sourceId:obj.options.useCamera});}
+if(bestFrameRate){opt.push({minFrameRate:bestFrameRate});opt.push({maxFrameRate:bestFrameRate});}
+video={mandatory:obj.options.videoParams,optional:opt};}}else{video={width:{min:obj.options.videoParams.minWidth,max:obj.options.videoParams.maxWidth},height:{min:obj.options.videoParams.minHeight,max:obj.options.videoParams.maxHeight}};var useVideo=obj.options.useVideo;if(useVideo&&obj.options.useCamera&&obj.options.useCamera!=="none"){if(obj.options.useCamera!=="any"){video.deviceId=obj.options.useCamera;}
+if(bestFrameRate){video.frameRate={ideal:bestFrameRate,min:minFrameRate,max:30};}}else{console.log("Camera Disabled");video=false;useVideo=false;}}
+return{audio:audio,video:video,useVideo:useVideo};}
+$.FSRTC.prototype.call=function(profile){checkCompat();var self=this;var screen=false;self.type="offer";if(self.options.videoParams&&self.options.screenShare){screen=true;}
+function onSuccess(stream){self.localStream=stream;if(screen){self.constraints.offerToReceiveVideo=false;self.constraints.offerToReceiveAudio=false;self.constraints.offerToSendAudio=false;}
+self.peer=FSRTCPeerConnection({type:self.type,attachStream:self.localStream,onICE:function(candidate){return onICE(self,candidate);},onICEComplete:function(){return onICEComplete(self);},onRemoteStream:screen?function(stream){}:function(stream){return onRemoteStream(self,stream);},onOfferSDP:function(sdp){return onOfferSDP(self,sdp);},onICESDP:function(sdp){return onICESDP(self,sdp);},onChannelError:function(e){return onChannelError(self,e);},constraints:self.constraints,iceServers:self.options.iceServers,});onStreamSuccess(self,stream);}
+function onError(e){onStreamError(self,e);}
+var mediaParams=getMediaParams(self);console.log("Audio constraints",mediaParams.audio);console.log("Video constraints",mediaParams.video);if(mediaParams.audio||mediaParams.video){getUserMedia({constraints:{audio:mediaParams.audio,video:mediaParams.video},video:mediaParams.useVideo,onsuccess:onSuccess,onerror:onError});}else{onSuccess(null);}};function FSRTCPeerConnection(options){var gathering=false,done=false;var config={};var default_ice={urls:['stun:stun.l.google.com:19302']};if(options.iceServers){if(typeof(options.iceServers)==="boolean"){config.iceServers=[default_ice];}else{config.iceServers=options.iceServers;}}
+var peer=new window.RTCPeerConnection(config);openOffererChannel();var x=0;function ice_handler(){done=true;gathering=null;if(options.onICEComplete){options.onICEComplete();}
+if(options.type=="offer"){options.onICESDP(peer.localDescription);}else{if(!x&&options.onICESDP){options.onICESDP(peer.localDescription);}}}
+peer.onicecandidate=function(event){if(done){return;}
+if(!gathering){gathering=setTimeout(ice_handler,1000);}
+if(event){if(event.candidate){options.onICE(event.candidate);}}else{done=true;if(gathering){clearTimeout(gathering);gathering=null;}
+ice_handler();}};if(options.attachStream)peer.addStream(options.attachStream);if(options.attachStreams&&options.attachStream.length){var streams=options.attachStreams;for(var i=0;i<streams.length;i++){peer.addStream(streams[i]);}}
+peer.onaddstream=function(event){var remoteMediaStream=event.stream;remoteMediaStream.oninactive=function(){if(options.onRemoteStreamEnded)options.onRemoteStreamEnded(remoteMediaStream);};if(options.onRemoteStream)options.onRemoteStream(remoteMediaStream);};function createOffer(){if(!options.onOfferSDP)return;peer.createOffer(function(sessionDescription){sessionDescription.sdp=serializeSdp(sessionDescription.sdp);peer.setLocalDescription(sessionDescription);options.onOfferSDP(sessionDescription);},onSdpError,options.constraints);}
+function createAnswer(){if(options.type!="answer")return;peer.setRemoteDescription(new window.RTCSessionDescription(options.offerSDP),onSdpSuccess,onSdpError);peer.createAnswer(function(sessionDescription){sessionDescription.sdp=serializeSdp(sessionDescription.sdp);peer.setLocalDescription(sessionDescription);if(options.onAnswerSDP){options.onAnswerSDP(sessionDescription);}},onSdpError);}
+if((options.onChannelMessage)||!options.onChannelMessage){createOffer();createAnswer();}
+function setBandwidth(sdp){sdp=sdp.replace(/b=AS([^\r\n]+\r\n)/g,'');sdp=sdp.replace(/a=mid:data\r\n/g,'a=mid:data\r\nb=AS:1638400\r\n');return sdp;}
+function getInteropSDP(sdp){var chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),extractedChars='';function getChars(){extractedChars+=chars[parseInt(Math.random()*40)]||'';if(extractedChars.length<40)getChars();return extractedChars;}
+if(options.onAnswerSDP)sdp=sdp.replace(/(a=crypto:0 AES_CM_128_HMAC_SHA1_32)(.*?)(\r\n)/g,'');var inline=getChars()+'\r\n'+(extractedChars='');sdp=sdp.indexOf('a=crypto')==-1?sdp.replace(/c=IN/g,'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:'+inline+'c=IN'):sdp;return sdp;}
+function serializeSdp(sdp){return sdp;}
+var channel;function openOffererChannel(){if(!options.onChannelMessage)return;_openOffererChannel();return;}
+function _openOffererChannel(){channel=peer.createDataChannel(options.channel||'RTCDataChannel',{reliable:false});setChannelEvents();}
+function setChannelEvents(){channel.onmessage=function(event){if(options.onChannelMessage)options.onChannelMessage(event);};channel.onopen=function(){if(options.onChannelOpened)options.onChannelOpened(channel);};channel.onclose=function(event){if(options.onChannelClosed)options.onChannelClosed(event);console.warn('WebRTC DataChannel closed',event);};channel.onerror=function(event){if(options.onChannelError)options.onChannelError(event);console.error('WebRTC DataChannel error',event);};}
+function openAnswererChannel(){peer.ondatachannel=function(event){channel=event.channel;channel.binaryType='blob';setChannelEvents();};return;}
+function useless(){log('Error in fake:true');}
+function onSdpSuccess(){}
+function onSdpError(e){if(options.onChannelError){options.onChannelError(e);}
+console.error('sdp error:',e);}
+return{addAnswerSDP:function(sdp,cbSuccess,cbError){peer.setRemoteDescription(new window.RTCSessionDescription(sdp),cbSuccess?cbSuccess:onSdpSuccess,cbError?cbError:onSdpError);},addICE:function(candidate){peer.addIceCandidate(new window.RTCIceCandidate({sdpMLineIndex:candidate.sdpMLineIndex,candidate:candidate.candidate}));},peer:peer,channel:channel,sendData:function(message){if(channel){channel.send(message);}},stop:function(){peer.close();if(options.attachStream){if(typeof options.attachStream.stop=='function'){options.attachStream.stop();}else{options.attachStream.active=false;}}}};}
+var video_constraints={};function getUserMedia(options){var n=navigator,media;n.getMedia=n.getUserMedia;n.getMedia(options.constraints||{audio:true,video:video_constraints},streaming,options.onerror||function(e){console.error(e);});function streaming(stream){if(options.localVideo){options.localVideo['src']=window.URL.createObjectURL(stream);options.localVideo.style.display='block';}
+if(options.onsuccess){options.onsuccess(stream);}
+media=stream;}
+return media;}
+$.FSRTC.resSupported=function(w,h){for(var i in $.FSRTC.validRes){if($.FSRTC.validRes[i][0]==w&&$.FSRTC.validRes[i][1]==h){return true;}}
+return false;}
+$.FSRTC.bestResSupported=function(){var w=0,h=0;for(var i in $.FSRTC.validRes){if($.FSRTC.validRes[i][0]>=w&&$.FSRTC.validRes[i][1]>=h){w=$.FSRTC.validRes[i][0];h=$.FSRTC.validRes[i][1];}}
+return[w,h];}
+var resList=[[160,120],[320,180],[320,240],[640,360],[640,480],[1280,720],[1920,1080]];var resI=0;var ttl=0;var checkRes=function(cam,func){if(resI>=resList.length){var res={'validRes':$.FSRTC.validRes,'bestResSupported':$.FSRTC.bestResSupported()};localStorage.setItem("res_"+cam,$.toJSON(res));if(func)return func(res);return;}
+var video={}
+if(cam){video.deviceId={exact:cam};}
+w=resList[resI][0];h=resList[resI][1];resI++;video={width:{exact:w},height:{exact:h}};getUserMedia({constraints:{audio:ttl++==0,video:video},onsuccess:function(e){e.getTracks().forEach(function(track){track.stop();});console.info(w+"x"+h+" supported.");$.FSRTC.validRes.push([w,h]);checkRes(cam,func);},onerror:function(e){console.warn(w+"x"+h+" not supported.");checkRes(cam,func);}});}
+$.FSRTC.getValidRes=function(cam,func){var used=[];var cached=localStorage.getItem("res_"+cam);if(cached){var cache=$.parseJSON(cached);if(cache){$.FSRTC.validRes=cache.validRes;console.log("CACHED RES FOR CAM "+cam,cache);}else{console.error("INVALID CACHE");}
+return func?func(cache):null;}
+$.FSRTC.validRes=[];resI=0;checkRes(cam,func);}
+$.FSRTC.checkPerms=function(runtime,check_audio,check_video){getUserMedia({constraints:{audio:check_audio,video:check_video,},onsuccess:function(e){e.getTracks().forEach(function(track){track.stop();});console.info("media perm init complete");if(runtime){setTimeout(runtime,100,true);}},onerror:function(e){if(check_video&&check_audio){console.error("error, retesting with audio params only");return $.FSRTC.checkPerms(runtime,check_audio,false);}
+console.error("media perm init error");if(runtime){runtime(false)}}});}})(jQuery);(function($){$.JsonRpcClient=function(options){var self=this;this.options=$.extend({ajaxUrl:null,socketUrl:null,onmessage:null,login:null,passwd:null,sessid:null,loginParams:null,userVariables:null,getSocket:function(onmessage_cb){return self._getSocket(onmessage_cb);}},options);self.ws_cnt=0;this.wsOnMessage=function(event){self._wsOnMessage(event);};};$.JsonRpcClient.prototype._ws_socket=null;$.JsonRpcClient.prototype._ws_callbacks={};$.JsonRpcClient.prototype._current_id=1;$.JsonRpcClient.prototype.speedTest=function(bytes,cb){var socket=this.options.getSocket(this.wsOnMessage);if(socket!==null){this.speedCB=cb;this.speedBytes=bytes;socket.send("#SPU "+bytes);var loops=bytes/1024;var rem=bytes%1024;var i;var data=new Array(1024).join(".");for(i=0;i<loops;i++){socket.send("#SPB "+data);}
+if(rem){socket.send("#SPB "+data);}
+socket.send("#SPE");}};$.JsonRpcClient.prototype.call=function(method,params,success_cb,error_cb){if(!params){params={};}
+if(this.options.sessid){params.sessid=this.options.sessid;}
+var request={jsonrpc:'2.0',method:method,params:params,id:this._current_id++};if(!success_cb){success_cb=function(e){console.log("Success: ",e);};}
+if(!error_cb){error_cb=function(e){console.log("Error: ",e);};}
+var socket=this.options.getSocket(this.wsOnMessage);if(socket!==null){this._wsCall(socket,request,success_cb,error_cb);return;}
+if(this.options.ajaxUrl===null){throw"$.JsonRpcClient.call used with no websocket and no http endpoint.";}
+$.ajax({type:'POST',url:this.options.ajaxUrl,data:$.toJSON(request),dataType:'json',cache:false,success:function(data){if('error'in data)error_cb(data.error,this);success_cb(data.result,this);},error:function(jqXHR,textStatus,errorThrown){try{var response=$.parseJSON(jqXHR.responseText);if('console'in window)console.log(response);error_cb(response.error,this);}catch(err){error_cb({error:jqXHR.responseText},this);}}});};$.JsonRpcClient.prototype.notify=function(method,params){if(this.options.sessid){params.sessid=this.options.sessid;}
+var request={jsonrpc:'2.0',method:method,params:params};var socket=this.options.getSocket(this.wsOnMessage);if(socket!==null){this._wsCall(socket,request);return;}
+if(this.options.ajaxUrl===null){throw"$.JsonRpcClient.notify used with no websocket and no http endpoint.";}
+$.ajax({type:'POST',url:this.options.ajaxUrl,data:$.toJSON(request),dataType:'json',cache:false});};$.JsonRpcClient.prototype.batch=function(callback,all_done_cb,error_cb){var batch=new $.JsonRpcClient._batchObject(this,all_done_cb,error_cb);callback(batch);batch._execute();};$.JsonRpcClient.prototype.socketReady=function(){if(this._ws_socket===null||this._ws_socket.readyState>1){return false;}
+return true;};$.JsonRpcClient.prototype.closeSocket=function(){var self=this;if(self.socketReady()){self._ws_socket.onclose=function(w){console.log("Closing Socket");};self._ws_socket.close();}};$.JsonRpcClient.prototype.loginData=function(params){var self=this;self.options.login=params.login;self.options.passwd=params.passwd;self.options.loginParams=params.loginParams;self.options.userVariables=params.userVariables;};$.JsonRpcClient.prototype.connectSocket=function(onmessage_cb){var self=this;if(self.to){clearTimeout(self.to);}
+if(!self.socketReady()){self.authing=false;if(self._ws_socket){delete self._ws_socket;}
+self._ws_socket=new WebSocket(self.options.socketUrl);if(self._ws_socket){self._ws_socket.onmessage=onmessage_cb;self._ws_socket.onclose=function(w){if(!self.ws_sleep){self.ws_sleep=1000;}
+if(self.options.onWSClose){self.options.onWSClose(self);}
+console.error("Websocket Lost "+self.ws_cnt+" sleep: "+self.ws_sleep+"msec");self.to=setTimeout(function(){console.log("Attempting Reconnection....");self.connectSocket(onmessage_cb);},self.ws_sleep);self.ws_cnt++;if(self.ws_sleep<3000&&(self.ws_cnt%10)===0){self.ws_sleep+=1000;}};self._ws_socket.onopen=function(){if(self.to){clearTimeout(self.to);}
+self.ws_sleep=1000;self.ws_cnt=0;if(self.options.onWSConnect){self.options.onWSConnect(self);}
+var req;while((req=$.JsonRpcClient.q.pop())){self._ws_socket.send(req);}};}}
+return self._ws_socket?true:false;};$.JsonRpcClient.prototype.stopRetrying=function(){if(self.to)
+clearTimeout(self.to);}
+$.JsonRpcClient.prototype._getSocket=function(onmessage_cb){if(this.options.socketUrl===null||!("WebSocket"in window))return null;this.connectSocket(onmessage_cb);return this._ws_socket;};$.JsonRpcClient.q=[];$.JsonRpcClient.prototype._wsCall=function(socket,request,success_cb,error_cb){var request_json=$.toJSON(request);if(socket.readyState<1){self=this;$.JsonRpcClient.q.push(request_json);}else{socket.send(request_json);}
+if('id'in request&&typeof success_cb!=='undefined'){this._ws_callbacks[request.id]={request:request_json,request_obj:request,success_cb:success_cb,error_cb:error_cb};}};$.JsonRpcClient.prototype._wsOnMessage=function(event){var response;if(event.data[0]=="#"&&event.data[1]=="S"&&event.data[2]=="P"){if(event.data[3]=="U"){this.up_dur=parseInt(event.data.substring(4));}else if(this.speedCB&&event.data[3]=="D"){this.down_dur=parseInt(event.data.substring(4));var up_kps=(((this.speedBytes*8)/(this.up_dur/1000))/1024).toFixed(0);var down_kps=(((this.speedBytes*8)/(this.down_dur/1000))/1024).toFixed(0);console.info("Speed Test: Up: "+up_kps+" Down: "+down_kps);this.speedCB(event,{upDur:this.up_dur,downDur:this.down_dur,upKPS:up_kps,downKPS:down_kps});this.speedCB=null;}
+return;}
+try{response=$.parseJSON(event.data);if(typeof response==='object'&&'jsonrpc'in response&&response.jsonrpc==='2.0'){if('result'in response&&this._ws_callbacks[response.id]){var success_cb=this._ws_callbacks[response.id].success_cb;delete this._ws_callbacks[response.id];success_cb(response.result,this);return;}else if('error'in response&&this._ws_callbacks[response.id]){var error_cb=this._ws_callbacks[response.id].error_cb;var orig_req=this._ws_callbacks[response.id].request;if(!self.authing&&response.error.code==-32000&&self.options.login&&self.options.passwd){self.authing=true;this.call("login",{login:self.options.login,passwd:self.options.passwd,loginParams:self.options.loginParams,userVariables:self.options.userVariables},this._ws_callbacks[response.id].request_obj.method=="login"?function(e){self.authing=false;console.log("logged in");delete self._ws_callbacks[response.id];if(self.options.onWSLogin){self.options.onWSLogin(true,self);}}:function(e){self.authing=false;console.log("logged in, resending request id: "+response.id);var socket=self.options.getSocket(self.wsOnMessage);if(socket!==null){socket.send(orig_req);}
+if(self.options.onWSLogin){self.options.onWSLogin(true,self);}},function(e){console.log("error logging in, request id:",response.id);delete self._ws_callbacks[response.id];error_cb(response.error,this);if(self.options.onWSLogin){self.options.onWSLogin(false,self);}});return;}
+delete this._ws_callbacks[response.id];error_cb(response.error,this);return;}}}catch(err){console.log("ERROR: "+err);return;}
+if(typeof this.options.onmessage==='function'){event.eventData=response;if(!event.eventData){event.eventData={};}
+var reply=this.options.onmessage(event);if(reply&&typeof reply==="object"&&event.eventData.id){var msg={jsonrpc:"2.0",id:event.eventData.id,result:reply};var socket=self.options.getSocket(self.wsOnMessage);if(socket!==null){socket.send($.toJSON(msg));}}}};$.JsonRpcClient._batchObject=function(jsonrpcclient,all_done_cb,error_cb){this._requests=[];this.jsonrpcclient=jsonrpcclient;this.all_done_cb=all_done_cb;this.error_cb=typeof error_cb==='function'?error_cb:function(){};};$.JsonRpcClient._batchObject.prototype.call=function(method,params,success_cb,error_cb){if(!params){params={};}
+if(this.options.sessid){params.sessid=this.options.sessid;}
+if(!success_cb){success_cb=function(e){console.log("Success: ",e);};}
+if(!error_cb){error_cb=function(e){console.log("Error: ",e);};}
+this._requests.push({request:{jsonrpc:'2.0',method:method,params:params,id:this.jsonrpcclient._current_id++},success_cb:success_cb,error_cb:error_cb});};$.JsonRpcClient._batchObject.prototype.notify=function(method,params){if(this.options.sessid){params.sessid=this.options.sessid;}
+this._requests.push({request:{jsonrpc:'2.0',method:method,params:params}});};$.JsonRpcClient._batchObject.prototype._execute=function(){var self=this;if(this._requests.length===0)return;var batch_request=[];var handlers={};var i=0;var call;var success_cb;var error_cb;var socket=self.jsonrpcclient.options.getSocket(self.jsonrpcclient.wsOnMessage);if(socket!==null){for(i=0;i<this._requests.length;i++){call=this._requests[i];success_cb=('success_cb'in call)?call.success_cb:undefined;error_cb=('error_cb'in call)?call.error_cb:undefined;self.jsonrpcclient._wsCall(socket,call.request,success_cb,error_cb);}
+if(typeof all_done_cb==='function')all_done_cb(result);return;}
+for(i=0;i<this._requests.length;i++){call=this._requests[i];batch_request.push(call.request);if('id'in call.request){handlers[call.request.id]={success_cb:call.success_cb,error_cb:call.error_cb};}}
+success_cb=function(data){self._batchCb(data,handlers,self.all_done_cb);};if(self.jsonrpcclient.options.ajaxUrl===null){throw"$.JsonRpcClient.batch used with no websocket and no http endpoint.";}
+$.ajax({url:self.jsonrpcclient.options.ajaxUrl,data:$.toJSON(batch_request),dataType:'json',cache:false,type:'POST',error:function(jqXHR,textStatus,errorThrown){self.error_cb(jqXHR,textStatus,errorThrown);},success:success_cb});};$.JsonRpcClient._batchObject.prototype._batchCb=function(result,handlers,all_done_cb){for(var i=0;i<result.length;i++){var response=result[i];if('error'in response){if(response.id===null||!(response.id in handlers)){if('console'in window)console.log(response);}else{handlers[response.id].error_cb(response.error,this);}}else{if(!(response.id in handlers)&&'console'in window){console.log(response);}else{handlers[response.id].success_cb(response.result,this);}}}
+if(typeof all_done_cb==='function')all_done_cb(result);};})(jQuery);(function($){var sources=[];var generateGUID=(typeof(window.crypto)!=='undefined'&&typeof(window.crypto.getRandomValues)!=='undefined')?function(){var buf=new Uint16Array(8);window.crypto.getRandomValues(buf);var S4=function(num){var ret=num.toString(16);while(ret.length<4){ret="0"+ret;}
+return ret;};return(S4(buf[0])+S4(buf[1])+"-"+S4(buf[2])+"-"+S4(buf[3])+"-"+S4(buf[4])+"-"+S4(buf[5])+S4(buf[6])+S4(buf[7]));}:function(){return'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);});};$.verto=function(options,callbacks){var verto=this;$.verto.saved.push(verto);verto.options=$.extend({login:null,passwd:null,socketUrl:null,tag:null,localTag:null,videoParams:{},audioParams:{},loginParams:{},deviceParams:{onResCheck:null},userVariables:{},iceServers:false,ringSleep:6000,sessid:null},options);if(verto.options.deviceParams.useCamera){$.FSRTC.getValidRes(verto.options.deviceParams.useCamera,verto.options.deviceParams.onResCheck);}
+if(!verto.options.deviceParams.useMic){verto.options.deviceParams.useMic="any";}
+if(!verto.options.deviceParams.useSpeak){verto.options.deviceParams.useSpeak="any";}
+if(verto.options.sessid){verto.sessid=verto.options.sessid;}else{verto.sessid=localStorage.getItem("verto_session_uuid")||generateGUID();localStorage.setItem("verto_session_uuid",verto.sessid);}
+verto.dialogs={};verto.callbacks=callbacks||{};verto.eventSUBS={};verto.rpcClient=new $.JsonRpcClient({login:verto.options.login,passwd:verto.options.passwd,socketUrl:verto.options.socketUrl,loginParams:verto.options.loginParams,userVariables:verto.options.userVariables,sessid:verto.sessid,onmessage:function(e){return verto.handleMessage(e.eventData);},onWSConnect:function(o){o.call('login',{});},onWSLogin:function(success){if(verto.callbacks.onWSLogin){verto.callbacks.onWSLogin(verto,success);}},onWSClose:function(success){if(verto.callbacks.onWSClose){verto.callbacks.onWSClose(verto,success);}
+verto.purge();}});var tag=verto.options.tag;if(typeof(tag)==="function"){tag=tag();}
+if(verto.options.ringFile&&verto.options.tag){verto.ringer=$("#"+tag);}
+verto.rpcClient.call('login',{});};$.verto.prototype.deviceParams=function(obj){var verto=this;for(var i in obj){verto.options.deviceParams[i]=obj[i];}
+if(obj.useCamera){$.FSRTC.getValidRes(verto.options.deviceParams.useCamera,obj?obj.onResCheck:undefined);}};$.verto.prototype.videoParams=function(obj){var verto=this;for(var i in obj){verto.options.videoParams[i]=obj[i];}};$.verto.prototype.iceServers=function(obj){var verto=this;verto.options.iceServers=obj;};$.verto.prototype.loginData=function(params){var verto=this;verto.options.login=params.login;verto.options.passwd=params.passwd;verto.rpcClient.loginData(params);};$.verto.prototype.logout=function(msg){var verto=this;verto.rpcClient.closeSocket();if(verto.callbacks.onWSClose){verto.callbacks.onWSClose(verto,false);}
+verto.purge();};$.verto.prototype.login=function(msg){var verto=this;verto.logout();verto.rpcClient.call('login',{});};$.verto.prototype.message=function(msg){var verto=this;var err=0;if(!msg.to){console.error("Missing To");err++;}
+if(!msg.body){console.error("Missing Body");err++;}
+if(err){return false;}
+verto.sendMethod("verto.info",{msg:msg});return true;};$.verto.prototype.processReply=function(method,success,e){var verto=this;var i;switch(method){case"verto.subscribe":for(i in e.unauthorizedChannels){drop_bad(verto,e.unauthorizedChannels[i]);}
+for(i in e.subscribedChannels){mark_ready(verto,e.subscribedChannels[i]);}
+break;case"verto.unsubscribe":break;}};$.verto.prototype.sendMethod=function(method,params){var verto=this;verto.rpcClient.call(method,params,function(e){verto.processReply(method,true,e);},function(e){verto.processReply(method,false,e);});};function do_sub(verto,channel,obj){}
+function drop_bad(verto,channel){console.error("drop unauthorized channel: "+channel);delete verto.eventSUBS[channel];}
+function mark_ready(verto,channel){for(var j in verto.eventSUBS[channel]){verto.eventSUBS[channel][j].ready=true;console.log("subscribed to channel: "+channel);if(verto.eventSUBS[channel][j].readyHandler){verto.eventSUBS[channel][j].readyHandler(verto,channel);}}}
+var SERNO=1;function do_subscribe(verto,channel,subChannels,sparams){var params=sparams||{};var local=params.local;var obj={eventChannel:channel,userData:params.userData,handler:params.handler,ready:false,readyHandler:params.readyHandler,serno:SERNO++};var isnew=false;if(!verto.eventSUBS[channel]){verto.eventSUBS[channel]=[];subChannels.push(channel);isnew=true;}
+verto.eventSUBS[channel].push(obj);if(local){obj.ready=true;obj.local=true;}
+if(!isnew&&verto.eventSUBS[channel][0].ready){obj.ready=true;if(obj.readyHandler){obj.readyHandler(verto,channel);}}
+return{serno:obj.serno,eventChannel:channel};}
+$.verto.prototype.subscribe=function(channel,sparams){var verto=this;var r=[];var subChannels=[];var params=sparams||{};if(typeof(channel)==="string"){r.push(do_subscribe(verto,channel,subChannels,params));}else{for(var i in channel){r.push(do_subscribe(verto,channel,subChannels,params));}}
+if(subChannels.length){verto.sendMethod("verto.subscribe",{eventChannel:subChannels.length==1?subChannels[0]:subChannels,subParams:params.subParams});}
+return r;};$.verto.prototype.unsubscribe=function(handle){var verto=this;var i;if(!handle){for(i in verto.eventSUBS){if(verto.eventSUBS[i]){verto.unsubscribe(verto.eventSUBS[i]);}}}else{var unsubChannels={};var sendChannels=[];var channel;if(typeof(handle)=="string"){delete verto.eventSUBS[handle];unsubChannels[handle]++;}else{for(i in handle){if(typeof(handle[i])=="string"){channel=handle[i];delete verto.eventSUBS[channel];unsubChannels[channel]++;}else{var repl=[];channel=handle[i].eventChannel;for(var j in verto.eventSUBS[channel]){if(verto.eventSUBS[channel][j].serno==handle[i].serno){}else{repl.push(verto.eventSUBS[channel][j]);}}
+verto.eventSUBS[channel]=repl;if(verto.eventSUBS[channel].length===0){delete verto.eventSUBS[channel];unsubChannels[channel]++;}}}}
+for(var u in unsubChannels){console.log("Sending Unsubscribe for: ",u);sendChannels.push(u);}
+if(sendChannels.length){verto.sendMethod("verto.unsubscribe",{eventChannel:sendChannels.length==1?sendChannels[0]:sendChannels});}}};$.verto.prototype.broadcast=function(channel,params){var verto=this;var msg={eventChannel:channel,data:{}};for(var i in params){msg.data[i]=params[i];}
+verto.sendMethod("verto.broadcast",msg);};$.verto.prototype.purge=function(callID){var verto=this;var x=0;var i;for(i in verto.dialogs){if(!x){console.log("purging dialogs");}
+x++;verto.dialogs[i].setState($.verto.enum.state.purge);}
+for(i in verto.eventSUBS){if(verto.eventSUBS[i]){console.log("purging subscription: "+i);delete verto.eventSUBS[i];}}};$.verto.prototype.hangup=function(callID){var verto=this;if(callID){var dialog=verto.dialogs[callID];if(dialog){dialog.hangup();}}else{for(var i in verto.dialogs){verto.dialogs[i].hangup();}}};$.verto.prototype.newCall=function(args,callbacks){var verto=this;if(!verto.rpcClient.socketReady()){console.error("Not Connected...");return;}
+var dialog=new $.verto.dialog($.verto.enum.direction.outbound,this,args);dialog.invite();if(callbacks){dialog.callbacks=callbacks;}
+return dialog;};$.verto.prototype.handleMessage=function(data){var verto=this;if(!(data&&data.method)){console.error("Invalid Data",data);return;}
+if(data.params.callID){var dialog=verto.dialogs[data.params.callID];if(data.method==="verto.attach"&&dialog){delete dialog.verto.dialogs[dialog.callID];dialog.rtc.stop();dialog=null;}
+if(dialog){switch(data.method){case'verto.bye':dialog.hangup(data.params);break;case'verto.answer':dialog.handleAnswer(data.params);break;case'verto.media':dialog.handleMedia(data.params);break;case'verto.display':dialog.handleDisplay(data.params);break;case'verto.info':dialog.handleInfo(data.params);break;default:console.debug("INVALID METHOD OR NON-EXISTANT CALL REFERENCE IGNORED",dialog,data.method);break;}}else{switch(data.method){case'verto.attach':data.params.attach=true;if(data.params.sdp&&data.params.sdp.indexOf("m=video")>0){data.params.useVideo=true;}
+if(data.params.sdp&&data.params.sdp.indexOf("stereo=1")>0){data.params.useStereo=true;}
+dialog=new $.verto.dialog($.verto.enum.direction.inbound,verto,data.params);dialog.setState($.verto.enum.state.recovering);break;case'verto.invite':if(data.params.sdp&&data.params.sdp.indexOf("m=video")>0){data.params.wantVideo=true;}
+if(data.params.sdp&&data.params.sdp.indexOf("stereo=1")>0){data.params.useStereo=true;}
+dialog=new $.verto.dialog($.verto.enum.direction.inbound,verto,data.params);break;default:console.debug("INVALID METHOD OR NON-EXISTANT CALL REFERENCE IGNORED");break;}}
+return{method:data.method};}else{switch(data.method){case'verto.punt':verto.purge();verto.logout();break;case'verto.event':var list=null;var key=null;if(data.params){key=data.params.eventChannel;}
+if(key){list=verto.eventSUBS[key];if(!list){list=verto.eventSUBS[key.split(".")[0]];}}
+if(!list&&key&&key===verto.sessid){if(verto.callbacks.onMessage){verto.callbacks.onMessage(verto,null,$.verto.enum.message.pvtEvent,data.params);}}else if(!list&&key&&verto.dialogs[key]){verto.dialogs[key].sendMessage($.verto.enum.message.pvtEvent,data.params);}else if(!list){if(!key){key="UNDEFINED";}
+console.error("UNSUBBED or invalid EVENT "+key+" IGNORED");}else{for(var i in list){var sub=list[i];if(!sub||!sub.ready){console.error("invalid EVENT for "+key+" IGNORED");}else if(sub.handler){sub.handler(verto,data.params,sub.userData);}else if(verto.callbacks.onEvent){verto.callbacks.onEvent(verto,data.params,sub.userData);}else{console.log("EVENT:",data.params);}}}
+break;case"verto.info":if(verto.callbacks.onMessage){verto.callbacks.onMessage(verto,null,$.verto.enum.message.info,data.params.msg);}
+console.debug("MESSAGE from: "+data.params.msg.from,data.params.msg.body);break;default:console.error("INVALID METHOD OR NON-EXISTANT CALL REFERENCE IGNORED",data.method);break;}}};var del_array=function(array,name){var r=[];var len=array.length;for(var i=0;i<len;i++){if(array[i]!=name){r.push(array[i]);}}
+return r;};var hashArray=function(){var vha=this;var hash={};var array=[];vha.reorder=function(a){array=a;var h=hash;hash={};var len=array.length;for(var i=0;i<len;i++){var key=array[i];if(h[key]){hash[key]=h[key];delete h[key];}}
+h=undefined;};vha.clear=function(){hash=undefined;array=undefined;hash={};array=[];};vha.add=function(name,val,insertAt){var redraw=false;if(!hash[name]){if(insertAt===undefined||insertAt<0||insertAt>=array.length){array.push(name);}else{var x=0;var n=[];var len=array.length;for(var i=0;i<len;i++){if(x++==insertAt){n.push(name);}
+n.push(array[i]);}
+array=undefined;array=n;n=undefined;redraw=true;}}
+hash[name]=val;return redraw;};vha.del=function(name){var r=false;if(hash[name]){array=del_array(array,name);delete hash[name];r=true;}else{console.error("can't del nonexistant key "+name);}
+return r;};vha.get=function(name){return hash[name];};vha.order=function(){return array;};vha.hash=function(){return hash;};vha.indexOf=function(name){var len=array.length;for(var i=0;i<len;i++){if(array[i]==name){return i;}}};vha.arrayLen=function(){return array.length;};vha.asArray=function(){var r=[];var len=array.length;for(var i=0;i<len;i++){var key=array[i];r.push(hash[key]);}
+return r;};vha.each=function(cb){var len=array.length;for(var i=0;i<len;i++){cb(array[i],hash[array[i]]);}};vha.dump=function(html){var str="";vha.each(function(name,val){str+="name: "+name+" val: "+JSON.stringify(val)+(html?"<br>":"\n");});return str;};};$.verto.liveArray=function(verto,context,name,config){var la=this;var lastSerno=0;var binding=null;var user_obj=config.userObj;var local=false;hashArray.call(la);la._add=la.add;la._del=la.del;la._reorder=la.reorder;la._clear=la.clear;la.context=context;la.name=name;la.user_obj=user_obj;la.verto=verto;la.broadcast=function(channel,obj){verto.broadcast(channel,obj);};la.errs=0;la.clear=function(){la._clear();lastSerno=0;if(la.onChange){la.onChange(la,{action:"clear"});}};la.checkSerno=function(serno){if(serno<0){return true;}
+if(lastSerno>0&&serno!=(lastSerno+1)){if(la.onErr){la.onErr(la,{lastSerno:lastSerno,serno:serno});}
+la.errs++;console.debug(la.errs);if(la.errs<3){la.bootstrap(la.user_obj);}
+return false;}else{lastSerno=serno;return true;}};la.reorder=function(serno,a){if(la.checkSerno(serno)){la._reorder(a);if(la.onChange){la.onChange(la,{serno:serno,action:"reorder"});}}};la.init=function(serno,val,key,index){if(key===null||key===undefined){key=serno;}
+if(la.checkSerno(serno)){if(la.onChange){la.onChange(la,{serno:serno,action:"init",index:index,key:key,data:val});}}};la.bootObj=function(serno,val){if(la.checkSerno(serno)){for(var i in val){la._add(val[i][0],val[i][1]);}
+if(la.onChange){la.onChange(la,{serno:serno,action:"bootObj",data:val,redraw:true});}}};la.add=function(serno,val,key,index){if(key===null||key===undefined){key=serno;}
+if(la.checkSerno(serno)){var redraw=la._add(key,val,index);if(la.onChange){la.onChange(la,{serno:serno,action:"add",index:index,key:key,data:val,redraw:redraw});}}};la.modify=function(serno,val,key,index){if(key===null||key===undefined){key=serno;}
+if(la.checkSerno(serno)){la._add(key,val,index);if(la.onChange){la.onChange(la,{serno:serno,action:"modify",key:key,data:val,index:index});}}};la.del=function(serno,key,index){if(key===null||key===undefined){key=serno;}
+if(la.checkSerno(serno)){if(index===null||index<0||index===undefined){index=la.indexOf(key);}
+var ok=la._del(key);if(ok&&la.onChange){la.onChange(la,{serno:serno,action:"del",key:key,index:index});}}};var eventHandler=function(v,e,la){var packet=e.data;if(packet.name!=la.name){return;}
+switch(packet.action){case"init":la.init(packet.wireSerno,packet.data,packet.hashKey,packet.arrIndex);break;case"bootObj":la.bootObj(packet.wireSerno,packet.data);break;case"add":la.add(packet.wireSerno,packet.data,packet.hashKey,packet.arrIndex);break;case"modify":if(!(packet.arrIndex||packet.hashKey)){console.error("Invalid Packet",packet);}else{la.modify(packet.wireSerno,packet.data,packet.hashKey,packet.arrIndex);}
+break;case"del":if(!(packet.arrIndex||packet.hashKey)){console.error("Invalid Packet",packet);}else{la.del(packet.wireSerno,packet.hashKey,packet.arrIndex);}
+break;case"clear":la.clear();break;case"reorder":la.reorder(packet.wireSerno,packet.order);break;default:if(la.checkSerno(packet.wireSerno)){if(la.onChange){la.onChange(la,{serno:packet.wireSerno,action:packet.action,data:packet.data});}}
+break;}};if(la.context){binding=la.verto.subscribe(la.context,{handler:eventHandler,userData:la,subParams:config.subParams});}
+la.destroy=function(){la._clear();la.verto.unsubscribe(binding);};la.sendCommand=function(cmd,obj){var self=la;self.broadcast(self.context,{liveArray:{command:cmd,context:self.context,name:self.name,obj:obj}});};la.bootstrap=function(obj){var self=la;la.sendCommand("bootstrap",obj);};la.changepage=function(obj){var self=la;self.clear();self.broadcast(self.context,{liveArray:{command:"changepage",context:la.context,name:la.name,obj:obj}});};la.heartbeat=function(obj){var self=la;var callback=function(){self.heartbeat.call(self,obj);};self.broadcast(self.context,{liveArray:{command:"heartbeat",context:self.context,name:self.name,obj:obj}});self.hb_pid=setTimeout(callback,30000);};la.bootstrap(la.user_obj);};$.verto.liveTable=function(verto,context,name,jq,config){var dt;var la=new $.verto.liveArray(verto,context,name,{subParams:config.subParams});var lt=this;lt.liveArray=la;lt.dataTable=dt;lt.verto=verto;lt.destroy=function(){if(dt){dt.fnDestroy();}
+if(la){la.destroy();}
+dt=null;la=null;};la.onErr=function(obj,args){console.error("Error: ",obj,args);};function genRow(data){if(typeof(data[4])==="string"&&data[4].indexOf("{")>-1){var tmp=$.parseJSON(data[4]);data[4]=tmp.oldStatus;data[5]=null;}
+return data;}
+function genArray(obj){var data=obj.asArray();for(var i in data){data[i]=genRow(data[i]);}
+return data;}
+la.onChange=function(obj,args){var index=0;var iserr=0;if(!dt){if(!config.aoColumns){if(args.action!="init"){return;}
+config.aoColumns=[];for(var i in args.data){config.aoColumns.push({"sTitle":args.data[i]});}}
+dt=jq.dataTable(config);}
+if(dt&&(args.action=="del"||args.action=="modify")){index=args.index;if(index===undefined&&args.key){index=la.indexOf(args.key);}
+if(index===undefined){console.error("INVALID PACKET Missing INDEX\n",args);return;}}
+if(config.onChange){config.onChange(obj,args);}
+try{switch(args.action){case"bootObj":if(!args.data){console.error("missing data");return;}
+dt.fnClearTable();dt.fnAddData(genArray(obj));dt.fnAdjustColumnSizing();break;case"add":if(!args.data){console.error("missing data");return;}
+if(args.redraw>-1){dt.fnClearTable();dt.fnAddData(genArray(obj));}else{dt.fnAddData(genRow(args.data));}
+dt.fnAdjustColumnSizing();break;case"modify":if(!args.data){return;}
+dt.fnUpdate(genRow(args.data),index);dt.fnAdjustColumnSizing();break;case"del":dt.fnDeleteRow(index);dt.fnAdjustColumnSizing();break;case"clear":dt.fnClearTable();break;case"reorder":dt.fnClearTable();dt.fnAddData(genArray(obj));break;case"hide":jq.hide();break;case"show":jq.show();break;}}catch(err){console.error("ERROR: "+err);iserr++;}
+if(iserr){obj.errs++;if(obj.errs<3){obj.bootstrap(obj.user_obj);}}else{obj.errs=0;}};la.onChange(la,{action:"init"});};var CONFMAN_SERNO=1;$.verto.conf=function(verto,params){var conf=this;conf.params=$.extend({dialog:null,hasVid:false,laData:null,onBroadcast:null,onLaChange:null,onLaRow:null},params);conf.verto=verto;conf.serno=CONFMAN_SERNO++;createMainModeratorMethods();verto.subscribe(conf.params.laData.modChannel,{handler:function(v,e){if(conf.params.onBroadcast){conf.params.onBroadcast(verto,conf,e.data);}}});verto.subscribe(conf.params.laData.infoChannel,{handler:function(v,e){if(typeof(conf.params.infoCallback)==="function"){conf.params.infoCallback(v,e);}}});verto.subscribe(conf.params.laData.chatChannel,{handler:function(v,e){if(typeof(conf.params.chatCallback)==="function"){conf.params.chatCallback(v,e);}}});};$.verto.conf.prototype.modCommand=function(cmd,id,value){var conf=this;conf.verto.rpcClient.call("verto.broadcast",{"eventChannel":conf.params.laData.modChannel,"data":{"application":"conf-control","command":cmd,"id":id,"value":value}});};$.verto.conf.prototype.destroy=function(){var conf=this;conf.destroyed=true;conf.params.onBroadcast(conf.verto,conf,'destroy');if(conf.params.laData.modChannel){conf.verto.unsubscribe(conf.params.laData.modChannel);}
+if(conf.params.laData.chatChannel){conf.verto.unsubscribe(conf.params.laData.chatChannel);}
+if(conf.params.laData.infoChannel){conf.verto.unsubscribe(conf.params.laData.infoChannel);}};function createMainModeratorMethods(){$.verto.conf.prototype.listVideoLayouts=function(){this.modCommand("list-videoLayouts",null,null);};$.verto.conf.prototype.play=function(file){this.modCommand("play",null,file);};$.verto.conf.prototype.stop=function(){this.modCommand("stop",null,"all");};$.verto.conf.prototype.deaf=function(memberID){this.modCommand("deaf",parseInt(memberID));};$.verto.conf.prototype.undeaf=function(memberID){this.modCommand("undeaf",parseInt(memberID));};$.verto.conf.prototype.record=function(file){this.modCommand("recording",null,["start",file]);};$.verto.conf.prototype.stopRecord=function(){this.modCommand("recording",null,["stop","all"]);};$.verto.conf.prototype.snapshot=function(file){if(!this.params.hasVid){throw'Conference has no video';}
+this.modCommand("vid-write-png",null,file);};$.verto.conf.prototype.setVideoLayout=function(layout,canvasID){if(!this.params.hasVid){throw'Conference has no video';}
+if(canvasID){this.modCommand("vid-layout",null,[layout,canvasID]);}else{this.modCommand("vid-layout",null,layout);}};$.verto.conf.prototype.kick=function(memberID){this.modCommand("kick",parseInt(memberID));};$.verto.conf.prototype.muteMic=function(memberID){this.modCommand("tmute",parseInt(memberID));};$.verto.conf.prototype.muteVideo=function(memberID){if(!this.params.hasVid){throw'Conference has no video';}
+this.modCommand("tvmute",parseInt(memberID));};$.verto.conf.prototype.presenter=function(memberID){if(!this.params.hasVid){throw'Conference has no video';}
+this.modCommand("vid-res-id",parseInt(memberID),"presenter");};$.verto.conf.prototype.videoFloor=function(memberID){if(!this.params.hasVid){throw'Conference has no video';}
+this.modCommand("vid-floor",parseInt(memberID),"force");};$.verto.conf.prototype.banner=function(memberID,text){if(!this.params.hasVid){throw'Conference has no video';}
+this.modCommand("vid-banner",parseInt(memberID),escape(text));};$.verto.conf.prototype.volumeDown=function(memberID){this.modCommand("volume_out",parseInt(memberID),"down");};$.verto.conf.prototype.volumeUp=function(memberID){this.modCommand("volume_out",parseInt(memberID),"up");};$.verto.conf.prototype.gainDown=function(memberID){this.modCommand("volume_in",parseInt(memberID),"down");};$.verto.conf.prototype.gainUp=function(memberID){this.modCommand("volume_in",parseInt(memberID),"up");};$.verto.conf.prototype.transfer=function(memberID,exten){this.modCommand("transfer",parseInt(memberID),exten);};$.verto.conf.prototype.sendChat=function(message,type){var conf=this;conf.verto.rpcClient.call("verto.broadcast",{"eventChannel":conf.params.laData.chatChannel,"data":{"action":"send","message":message,"type":type}});};}
+$.verto.modfuncs={};$.verto.confMan=function(verto,params){var confMan=this;confMan.params=$.extend({tableID:null,statusID:null,mainModID:null,dialog:null,hasVid:false,laData:null,onBroadcast:null,onLaChange:null,onLaRow:null},params);confMan.verto=verto;confMan.serno=CONFMAN_SERNO++;confMan.canvasCount=confMan.params.laData.canvasCount;function genMainMod(jq){var play_id="play_"+confMan.serno;var stop_id="stop_"+confMan.serno;var recording_id="recording_"+confMan.serno;var snapshot_id="snapshot_"+confMan.serno;var rec_stop_id="recording_stop"+confMan.serno;var div_id="confman_"+confMan.serno;var html="<div id='"+div_id+"'><br>"+"<button class='ctlbtn' id='"+play_id+"'>Play</button>"+"<button class='ctlbtn' id='"+stop_id+"'>Stop</button>"+"<button class='ctlbtn' id='"+recording_id+"'>Record</button>"+"<button class='ctlbtn' id='"+rec_stop_id+"'>Record Stop</button>"+
+(confMan.params.hasVid?"<button class='ctlbtn' id='"+snapshot_id+"'>PNG Snapshot</button>":"")+"<br><br></div>";jq.html(html);$.verto.modfuncs.change_video_layout=function(id,canvas_id){var val=$("#"+id+" option:selected").text();if(val!=="none"){confMan.modCommand("vid-layout",null,[val,canvas_id]);}};if(confMan.params.hasVid){for(var j=0;j<confMan.canvasCount;j++){var vlayout_id="confman_vid_layout_"+j+"_"+confMan.serno;var vlselect_id="confman_vl_select_"+j+"_"+confMan.serno;var vlhtml="<div id='"+vlayout_id+"'><br>"+"<b>Video Layout Canvas "+(j+1)+"</b> <select onChange='$.verto.modfuncs.change_video_layout(\""+vlayout_id+"\", \""+(j+1)+"\")' id='"+vlselect_id+"'></select> "+"<br><br></div>";jq.append(vlhtml);}
+$("#"+snapshot_id).click(function(){var file=prompt("Please enter file name","");if(file){confMan.modCommand("vid-write-png",null,file);}});}
+$("#"+play_id).click(function(){var file=prompt("Please enter file name","");if(file){confMan.modCommand("play",null,file);}});$("#"+stop_id).click(function(){confMan.modCommand("stop",null,"all");});$("#"+recording_id).click(function(){var file=prompt("Please enter file name","");if(file){confMan.modCommand("recording",null,["start",file]);}});$("#"+rec_stop_id).click(function(){confMan.modCommand("recording",null,["stop","all"]);});}
+function genControls(jq,rowid){var x=parseInt(rowid);var kick_id="kick_"+x;var canvas_in_next_id="canvas_in_next_"+x;var canvas_in_prev_id="canvas_in_prev_"+x;var canvas_out_next_id="canvas_out_next_"+x;var canvas_out_prev_id="canvas_out_prev_"+x;var canvas_in_set_id="canvas_in_set_"+x;var canvas_out_set_id="canvas_out_set_"+x;var layer_set_id="layer_set_"+x;var layer_next_id="layer_next_"+x;var layer_prev_id="layer_prev_"+x;var tmute_id="tmute_"+x;var tvmute_id="tvmute_"+x;var vbanner_id="vbanner_"+x;var tvpresenter_id="tvpresenter_"+x;var tvfloor_id="tvfloor_"+x;var box_id="box_"+x;var gainup_id="gain_in_up"+x;var gaindn_id="gain_in_dn"+x;var volup_id="vol_in_up"+x;var voldn_id="vol_in_dn"+x;var transfer_id="transfer"+x;var html="<div id='"+box_id+"'>";html+="<b>General Controls</b><hr noshade>";html+="<button class='ctlbtn' id='"+kick_id+"'>Kick</button>"+"<button class='ctlbtn' id='"+tmute_id+"'>Mute</button>"+"<button class='ctlbtn' id='"+gainup_id+"'>Gain -</button>"+"<button class='ctlbtn' id='"+gaindn_id+"'>Gain +</button>"+"<button class='ctlbtn' id='"+voldn_id+"'>Vol -</button>"+"<button class='ctlbtn' id='"+volup_id+"'>Vol +</button>"+"<button class='ctlbtn' id='"+transfer_id+"'>Transfer</button>";if(confMan.params.hasVid){html+="<br><br><b>Video Controls</b><hr noshade>";html+="<button class='ctlbtn' id='"+tvmute_id+"'>VMute</button>"+"<button class='ctlbtn' id='"+tvpresenter_id+"'>Presenter</button>"+"<button class='ctlbtn' id='"+tvfloor_id+"'>Vid Floor</button>"+"<button class='ctlbtn' id='"+vbanner_id+"'>Banner</button>";if(confMan.canvasCount>1){html+="<br><br><b>Canvas Controls</b><hr noshade>"+"<button class='ctlbtn' id='"+canvas_in_set_id+"'>Set Input Canvas</button>"+"<button class='ctlbtn' id='"+canvas_in_prev_id+"'>Prev Input Canvas</button>"+"<button class='ctlbtn' id='"+canvas_in_next_id+"'>Next Input Canvas</button>"+"<br>"+"<button class='ctlbtn' id='"+canvas_out_set_id+"'>Set Watching Canvas</button>"+"<button class='ctlbtn' id='"+canvas_out_prev_id+"'>Prev Watching Canvas</button>"+"<button class='ctlbtn' id='"+canvas_out_next_id+"'>Next Watching Canvas</button>";}
+html+="<br>"+"<button class='ctlbtn' id='"+layer_set_id+"'>Set Layer</button>"+"<button class='ctlbtn' id='"+layer_prev_id+"'>Prev Layer</button>"+"<button class='ctlbtn' id='"+layer_next_id+"'>Next Layer</button>"+"</div>";}
+jq.html(html);if(!jq.data("mouse")){$("#"+box_id).hide();}
+jq.mouseover(function(e){jq.data({"mouse":true});$("#"+box_id).show();});jq.mouseout(function(e){jq.data({"mouse":false});$("#"+box_id).hide();});$("#"+transfer_id).click(function(){var xten=prompt("Enter Extension");if(xten){confMan.modCommand("transfer",x,xten);}});$("#"+kick_id).click(function(){confMan.modCommand("kick",x);});$("#"+layer_set_id).click(function(){var cid=prompt("Please enter layer ID","");if(cid){confMan.modCommand("vid-layer",x,cid);}});$("#"+layer_next_id).click(function(){confMan.modCommand("vid-layer",x,"next");});$("#"+layer_prev_id).click(function(){confMan.modCommand("vid-layer",x,"prev");});$("#"+canvas_in_set_id).click(function(){var cid=prompt("Please enter canvas ID","");if(cid){confMan.modCommand("vid-canvas",x,cid);}});$("#"+canvas_out_set_id).click(function(){var cid=prompt("Please enter canvas ID","");if(cid){confMan.modCommand("vid-watching-canvas",x,cid);}});$("#"+canvas_in_next_id).click(function(){confMan.modCommand("vid-canvas",x,"next");});$("#"+canvas_in_prev_id).click(function(){confMan.modCommand("vid-canvas",x,"prev");});$("#"+canvas_out_next_id).click(function(){confMan.modCommand("vid-watching-canvas",x,"next");});$("#"+canvas_out_prev_id).click(function(){confMan.modCommand("vid-watching-canvas",x,"prev");});$("#"+tmute_id).click(function(){confMan.modCommand("tmute",x);});if(confMan.params.hasVid){$("#"+tvmute_id).click(function(){confMan.modCommand("tvmute",x);});$("#"+tvpresenter_id).click(function(){confMan.modCommand("vid-res-id",x,"presenter");});$("#"+tvfloor_id).click(function(){confMan.modCommand("vid-floor",x,"force");});$("#"+vbanner_id).click(function(){var text=prompt("Please enter text","");if(text){confMan.modCommand("vid-banner",x,escape(text));}});}
+$("#"+gainup_id).click(function(){confMan.modCommand("volume_in",x,"up");});$("#"+gaindn_id).click(function(){confMan.modCommand("volume_in",x,"down");});$("#"+volup_id).click(function(){confMan.modCommand("volume_out",x,"up");});$("#"+voldn_id).click(function(){confMan.modCommand("volume_out",x,"down");});return html;}
+var atitle="";var awidth=0;verto.subscribe(confMan.params.laData.infoChannel,{handler:function(v,e){if(typeof(confMan.params.infoCallback)==="function"){confMan.params.infoCallback(v,e);}}});verto.subscribe(confMan.params.laData.chatChannel,{handler:function(v,e){if(typeof(confMan.params.chatCallback)==="function"){confMan.params.chatCallback(v,e);}}});if(confMan.params.laData.role==="moderator"){atitle="Action";awidth=600;if(confMan.params.mainModID){genMainMod($(confMan.params.mainModID));$(confMan.params.displayID).html("Moderator Controls Ready<br><br>");}else{$(confMan.params.mainModID).html("");}
+verto.subscribe(confMan.params.laData.modChannel,{handler:function(v,e){if(confMan.params.onBroadcast){confMan.params.onBroadcast(verto,confMan,e.data);}
+if(e.data["conf-command"]==="list-videoLayouts"){for(var j=0;j<confMan.canvasCount;j++){var vlselect_id="#confman_vl_select_"+j+"_"+confMan.serno;var vlayout_id="#confman_vid_layout_"+j+"_"+confMan.serno;var x=0;var options;$(vlselect_id).selectmenu({});$(vlselect_id).selectmenu("enable");$(vlselect_id).empty();$(vlselect_id).append(new Option("Choose a Layout","none"));if(e.data.responseData){var rdata=[];for(var i in e.data.responseData){rdata.push(e.data.responseData[i].name);}
+options=rdata.sort(function(a,b){var ga=a.substring(0,6)=="group:"?true:false;var gb=b.substring(0,6)=="group:"?true:false;if((ga||gb)&&ga!=gb){return ga?-1:1;}
+return((a==b)?0:((a>b)?1:-1));});for(var i in options){$(vlselect_id).append(new Option(options[i],options[i]));x++;}}
+if(x){$(vlselect_id).selectmenu('refresh',true);}else{$(vlayout_id).hide();}}}else{if(!confMan.destroyed&&confMan.params.displayID){$(confMan.params.displayID).html(e.data.response+"<br><br>");if(confMan.lastTimeout){clearTimeout(confMan.lastTimeout);confMan.lastTimeout=0;}
+confMan.lastTimeout=setTimeout(function(){$(confMan.params.displayID).html(confMan.destroyed?"":"Moderator Controls Ready<br><br>");},4000);}}}});if(confMan.params.hasVid){confMan.modCommand("list-videoLayouts",null,null);}}
+var row_callback=null;if(confMan.params.laData.role==="moderator"){row_callback=function(nRow,aData,iDisplayIndex,iDisplayIndexFull){if(!aData[5]){var $row=$('td:eq(5)',nRow);genControls($row,aData);if(confMan.params.onLaRow){confMan.params.onLaRow(verto,confMan,$row,aData);}}};}
+confMan.lt=new $.verto.liveTable(verto,confMan.params.laData.laChannel,confMan.params.laData.laName,$(confMan.params.tableID),{subParams:{callID:confMan.params.dialog?confMan.params.dialog.callID:null},"onChange":function(obj,args){$(confMan.params.statusID).text("Conference Members: "+" ("+obj.arrayLen()+" Total)");if(confMan.params.onLaChange){confMan.params.onLaChange(verto,confMan,$.verto.enum.confEvent.laChange,obj,args);}},"aaData":[],"aoColumns":[{"sTitle":"ID","sWidth":"50"},{"sTitle":"Number","sWidth":"250"},{"sTitle":"Name","sWidth":"250"},{"sTitle":"Codec","sWidth":"100"},{"sTitle":"Status","sWidth":confMan.params.hasVid?"200px":"150px"},{"sTitle":atitle,"sWidth":awidth,}],"bAutoWidth":true,"bDestroy":true,"bSort":false,"bInfo":false,"bFilter":false,"bLengthChange":false,"bPaginate":false,"iDisplayLength":1400,"oLanguage":{"sEmptyTable":"The Conference is Empty....."},"fnRowCallback":row_callback});};$.verto.confMan.prototype.modCommand=function(cmd,id,value){var confMan=this;confMan.verto.rpcClient.call("verto.broadcast",{"eventChannel":confMan.params.laData.modChannel,"data":{"application":"conf-control","command":cmd,"id":id,"value":value}});};$.verto.confMan.prototype.sendChat=function(message,type){var confMan=this;confMan.verto.rpcClient.call("verto.broadcast",{"eventChannel":confMan.params.laData.chatChannel,"data":{"action":"send","message":message,"type":type}});};$.verto.confMan.prototype.destroy=function(){var confMan=this;confMan.destroyed=true;if(confMan.lt){confMan.lt.destroy();}
+if(confMan.params.laData.chatChannel){confMan.verto.unsubscribe(confMan.params.laData.chatChannel);}
+if(confMan.params.laData.modChannel){confMan.verto.unsubscribe(confMan.params.laData.modChannel);}
+if(confMan.params.mainModID){$(confMan.params.mainModID).html("");}};$.verto.dialog=function(direction,verto,params){var dialog=this;dialog.params=$.extend({useVideo:verto.options.useVideo,useStereo:verto.options.useStereo,screenShare:false,useCamera:false,useMic:verto.options.deviceParams.useMic,useSpeak:verto.options.deviceParams.useSpeak,tag:verto.options.tag,localTag:verto.options.localTag,login:verto.options.login,videoParams:verto.options.videoParams},params);if(!dialog.params.screenShare){dialog.params.useCamera=verto.options.deviceParams.useCamera;}
+dialog.verto=verto;dialog.direction=direction;dialog.lastState=null;dialog.state=dialog.lastState=$.verto.enum.state.new;dialog.callbacks=verto.callbacks;dialog.answered=false;dialog.attach=params.attach||false;dialog.screenShare=params.screenShare||false;dialog.useCamera=dialog.params.useCamera;dialog.useMic=dialog.params.useMic;dialog.useSpeak=dialog.params.useSpeak;if(dialog.params.callID){dialog.callID=dialog.params.callID;}else{dialog.callID=dialog.params.callID=generateGUID();}
+if(typeof(dialog.params.tag)==="function"){dialog.params.tag=dialog.params.tag();}
+if(dialog.params.tag){dialog.audioStream=document.getElementById(dialog.params.tag);if(dialog.params.useVideo){dialog.videoStream=dialog.audioStream;}}
+if(dialog.params.localTag){dialog.localVideo=document.getElementById(dialog.params.localTag);}
+dialog.verto.dialogs[dialog.callID]=dialog;var RTCcallbacks={};if(dialog.direction==$.verto.enum.direction.inbound){if(dialog.params.display_direction==="outbound"){dialog.params.remote_caller_id_name=dialog.params.caller_id_name;dialog.params.remote_caller_id_number=dialog.params.caller_id_number;}else{dialog.params.remote_caller_id_name=dialog.params.callee_id_name;dialog.params.remote_caller_id_number=dialog.params.callee_id_number;}
+if(!dialog.params.remote_caller_id_name){dialog.params.remote_caller_id_name="Nobody";}
+if(!dialog.params.remote_caller_id_number){dialog.params.remote_caller_id_number="UNKNOWN";}
+RTCcallbacks.onMessage=function(rtc,msg){console.debug(msg);};RTCcallbacks.onAnswerSDP=function(rtc,sdp){console.error("answer sdp",sdp);};}else{dialog.params.remote_caller_id_name="Outbound Call";dialog.params.remote_caller_id_number=dialog.params.destination_number;}
+RTCcallbacks.onICESDP=function(rtc){console.log("RECV "+rtc.type+" SDP",rtc.mediaData.SDP);if(dialog.state==$.verto.enum.state.requesting||dialog.state==$.verto.enum.state.answering||dialog.state==$.verto.enum.state.active){location.reload();return;}
+if(rtc.type=="offer"){if(dialog.state==$.verto.enum.state.active){dialog.setState($.verto.enum.state.requesting);dialog.sendMethod("verto.attach",{sdp:rtc.mediaData.SDP});}else{dialog.setState($.verto.enum.state.requesting);dialog.sendMethod("verto.invite",{sdp:rtc.mediaData.SDP});}}else{dialog.setState($.verto.enum.state.answering);dialog.sendMethod(dialog.attach?"verto.attach":"verto.answer",{sdp:dialog.rtc.mediaData.SDP});}};RTCcallbacks.onICE=function(rtc){if(rtc.type=="offer"){console.log("offer",rtc.mediaData.candidate);return;}};RTCcallbacks.onStream=function(rtc,stream){if(dialog.verto.options.permissionCallback&&typeof dialog.verto.options.permissionCallback.onGranted==='function'){dialog.verto.options.permissionCallback.onGranted(stream);}
+console.log("stream started");};RTCcallbacks.onError=function(e){if(dialog.verto.options.permissionCallback&&typeof dialog.verto.options.permissionCallback.onDenied==='function'){dialog.verto.options.permissionCallback.onDenied();}
+console.error("ERROR:",e);dialog.hangup({cause:"Device or Permission Error"});};dialog.rtc=new $.FSRTC({callbacks:RTCcallbacks,localVideo:dialog.screenShare?null:dialog.localVideo,useVideo:dialog.params.useVideo?dialog.videoStream:null,useAudio:dialog.audioStream,useStereo:dialog.params.useStereo,videoParams:dialog.params.videoParams,audioParams:verto.options.audioParams,iceServers:verto.options.iceServers,screenShare:dialog.screenShare,useCamera:dialog.useCamera,useMic:dialog.useMic,useSpeak:dialog.useSpeak});dialog.rtc.verto=dialog.verto;if(dialog.direction==$.verto.enum.direction.inbound){if(dialog.attach){dialog.answer();}else{dialog.ring();}}};$.verto.dialog.prototype.invite=function(){var dialog=this;dialog.rtc.call();};$.verto.dialog.prototype.sendMethod=function(method,obj){var dialog=this;obj.dialogParams={};for(var i in dialog.params){if(i=="sdp"&&method!="verto.invite"&&method!="verto.attach"){continue;}
+if((obj.noDialogParams&&i!="callID")){continue;}
+obj.dialogParams[i]=dialog.params[i];}
+delete obj.noDialogParams;dialog.verto.rpcClient.call(method,obj,function(e){dialog.processReply(method,true,e);},function(e){dialog.processReply(method,false,e);});};function checkStateChange(oldS,newS){if(newS==$.verto.enum.state.purge||$.verto.enum.states[oldS.name][newS.name]){return true;}
+return false;}
+function find_name(id){for(var i in $.verto.audioOutDevices){var source=$.verto.audioOutDevices[i];if(source.id===id){return(source.label);}}
+return id;}
+$.verto.dialog.prototype.setAudioPlaybackDevice=function(sinkId,callback,arg){var dialog=this;var element=dialog.audioStream;if(typeof element.sinkId!=='undefined'){var devname=find_name(sinkId);console.info("Dialog: "+dialog.callID+" Setting speaker:",element,devname);element.setSinkId(sinkId).then(function(){console.log("Dialog: "+dialog.callID+' Success, audio output device attached: '+sinkId);if(callback){callback(true,devname,arg);}}).catch(function(error){var errorMessage=error;if(error.name==='SecurityError'){errorMessage="Dialog: "+dialog.callID+' You need to use HTTPS for selecting audio output '+'device: '+error;}
+if(callback){callback(false,null,arg);}
+console.error(errorMessage);});}else{console.warn("Dialog: "+dialog.callID+' Browser does not support output device selection.');if(callback){callback(false,null,arg);}}}
+$.verto.dialog.prototype.setState=function(state){var dialog=this;if(dialog.state==$.verto.enum.state.ringing){dialog.stopRinging();}
+if(dialog.state==state||!checkStateChange(dialog.state,state)){console.error("Dialog "+dialog.callID+": INVALID state change from "+dialog.state.name+" to "+state.name);dialog.hangup();return false;}
+console.log("Dialog "+dialog.callID+": state change from "+dialog.state.name+" to "+state.name);dialog.lastState=dialog.state;dialog.state=state;if(!dialog.causeCode){dialog.causeCode=16;}
+if(!dialog.cause){dialog.cause="NORMAL CLEARING";}
+if(dialog.callbacks.onDialogState){dialog.callbacks.onDialogState(this);}
+switch(dialog.state){case $.verto.enum.state.early:case $.verto.enum.state.active:var speaker=dialog.useSpeak;console.info("Using Speaker: ",speaker);if(speaker&&speaker!=="any"&&speaker!=="none"){setTimeout(function(){dialog.setAudioPlaybackDevice(speaker);},500);}
+break;case $.verto.enum.state.trying:setTimeout(function(){if(dialog.state==$.verto.enum.state.trying){dialog.setState($.verto.enum.state.hangup);}},30000);break;case $.verto.enum.state.purge:dialog.setState($.verto.enum.state.destroy);break;case $.verto.enum.state.hangup:if(dialog.lastState.val>$.verto.enum.state.requesting.val&&dialog.lastState.val<$.verto.enum.state.hangup.val){dialog.sendMethod("verto.bye",{});}
+dialog.setState($.verto.enum.state.destroy);break;case $.verto.enum.state.destroy:if(typeof(dialog.verto.options.tag)==="function"){$('#'+dialog.params.tag).remove();}
+delete dialog.verto.dialogs[dialog.callID];if(dialog.params.screenShare){dialog.rtc.stopPeer();}else{dialog.rtc.stop();}
+break;}
+return true;};$.verto.dialog.prototype.processReply=function(method,success,e){var dialog=this;switch(method){case"verto.answer":case"verto.attach":if(success){dialog.setState($.verto.enum.state.active);}else{dialog.hangup();}
+break;case"verto.invite":if(success){dialog.setState($.verto.enum.state.trying);}else{dialog.setState($.verto.enum.state.destroy);}
+break;case"verto.bye":dialog.hangup();break;case"verto.modify":if(e.holdState){if(e.holdState=="held"){if(dialog.state!=$.verto.enum.state.held){dialog.setState($.verto.enum.state.held);}}else if(e.holdState=="active"){if(dialog.state!=$.verto.enum.state.active){dialog.setState($.verto.enum.state.active);}}}
+if(success){}
+break;default:break;}};$.verto.dialog.prototype.hangup=function(params){var dialog=this;if(params){if(params.causeCode){dialog.causeCode=params.causeCode;}
+if(params.cause){dialog.cause=params.cause;}}
+if(dialog.state.val>=$.verto.enum.state.new.val&&dialog.state.val<$.verto.enum.state.hangup.val){dialog.setState($.verto.enum.state.hangup);}else if(dialog.state.val<$.verto.enum.state.destroy){dialog.setState($.verto.enum.state.destroy);}};$.verto.dialog.prototype.stopRinging=function(){var dialog=this;if(dialog.verto.ringer){dialog.verto.ringer.stop();}};$.verto.dialog.prototype.indicateRing=function(){var dialog=this;if(dialog.verto.ringer){dialog.verto.ringer.attr("src",dialog.verto.options.ringFile)[0].play();setTimeout(function(){dialog.stopRinging();if(dialog.state==$.verto.enum.state.ringing){dialog.indicateRing();}},dialog.verto.options.ringSleep);}};$.verto.dialog.prototype.ring=function(){var dialog=this;dialog.setState($.verto.enum.state.ringing);dialog.indicateRing();};$.verto.dialog.prototype.useVideo=function(on){var dialog=this;dialog.params.useVideo=on;if(on){dialog.videoStream=dialog.audioStream;}else{dialog.videoStream=null;}
+dialog.rtc.useVideo(dialog.videoStream,dialog.localVideo);};$.verto.dialog.prototype.setMute=function(what){var dialog=this;return dialog.rtc.setMute(what);};$.verto.dialog.prototype.getMute=function(){var dialog=this;return dialog.rtc.getMute();};$.verto.dialog.prototype.setVideoMute=function(what){var dialog=this;return dialog.rtc.setVideoMute(what);};$.verto.dialog.prototype.getVideoMute=function(){var dialog=this;return dialog.rtc.getVideoMute();};$.verto.dialog.prototype.useStereo=function(on){var dialog=this;dialog.params.useStereo=on;dialog.rtc.useStereo(on);};$.verto.dialog.prototype.dtmf=function(digits){var dialog=this;if(digits){dialog.sendMethod("verto.info",{dtmf:digits});}};$.verto.dialog.prototype.rtt=function(obj){var dialog=this;var pobj={};if(!obj){return false;}
+pobj.code=obj.code;pobj.chars=obj.chars;if(pobj.chars||pobj.code){dialog.sendMethod("verto.info",{txt:obj,noDialogParams:true});}};$.verto.dialog.prototype.transfer=function(dest,params){var dialog=this;if(dest){dialog.sendMethod("verto.modify",{action:"transfer",destination:dest,params:params});}};$.verto.dialog.prototype.hold=function(params){var dialog=this;dialog.sendMethod("verto.modify",{action:"hold",params:params});};$.verto.dialog.prototype.unhold=function(params){var dialog=this;dialog.sendMethod("verto.modify",{action:"unhold",params:params});};$.verto.dialog.prototype.toggleHold=function(params){var dialog=this;dialog.sendMethod("verto.modify",{action:"toggleHold",params:params});};$.verto.dialog.prototype.message=function(msg){var dialog=this;var err=0;msg.from=dialog.params.login;if(!msg.to){console.error("Missing To");err++;}
+if(!msg.body){console.error("Missing Body");err++;}
+if(err){return false;}
+dialog.sendMethod("verto.info",{msg:msg});return true;};$.verto.dialog.prototype.answer=function(params){var dialog=this;if(!dialog.answered){if(!params){params={};}
+params.sdp=dialog.params.sdp;if(params){if(params.useVideo){dialog.useVideo(true);}
+dialog.params.callee_id_name=params.callee_id_name;dialog.params.callee_id_number=params.callee_id_number;if(params.useCamera){dialog.useCamera=params.useCamera;}
+if(params.useMic){dialog.useMic=params.useMic;}
+if(params.useSpeak){dialog.useSpeak=params.useSpeak;}}
+dialog.rtc.createAnswer(params);dialog.answered=true;}};$.verto.dialog.prototype.handleAnswer=function(params){var dialog=this;dialog.gotAnswer=true;if(dialog.state.val>=$.verto.enum.state.active.val){return;}
+if(dialog.state.val>=$.verto.enum.state.early.val){dialog.setState($.verto.enum.state.active);}else{if(dialog.gotEarly){console.log("Dialog "+dialog.callID+" Got answer while still establishing early media, delaying...");}else{console.log("Dialog "+dialog.callID+" Answering Channel");dialog.rtc.answer(params.sdp,function(){dialog.setState($.verto.enum.state.active);},function(e){console.error(e);dialog.hangup();});console.log("Dialog "+dialog.callID+"ANSWER SDP",params.sdp);}}};$.verto.dialog.prototype.cidString=function(enc){var dialog=this;var party=dialog.params.remote_caller_id_name+(enc?" &lt;":" <")+dialog.params.remote_caller_id_number+(enc?"&gt;":">");return party;};$.verto.dialog.prototype.sendMessage=function(msg,params){var dialog=this;if(dialog.callbacks.onMessage){dialog.callbacks.onMessage(dialog.verto,dialog,msg,params);}};$.verto.dialog.prototype.handleInfo=function(params){var dialog=this;dialog.sendMessage($.verto.enum.message.info,params);};$.verto.dialog.prototype.handleDisplay=function(params){var dialog=this;if(params.display_name){dialog.params.remote_caller_id_name=params.display_name;}
+if(params.display_number){dialog.params.remote_caller_id_number=params.display_number;}
+dialog.sendMessage($.verto.enum.message.display,{});};$.verto.dialog.prototype.handleMedia=function(params){var dialog=this;if(dialog.state.val>=$.verto.enum.state.early.val){return;}
+dialog.gotEarly=true;dialog.rtc.answer(params.sdp,function(){console.log("Dialog "+dialog.callID+"Establishing early media");dialog.setState($.verto.enum.state.early);if(dialog.gotAnswer){console.log("Dialog "+dialog.callID+"Answering Channel");dialog.setState($.verto.enum.state.active);}},function(e){console.error(e);dialog.hangup();});console.log("Dialog "+dialog.callID+"EARLY SDP",params.sdp);};$.verto.ENUM=function(s){var i=0,o={};s.split(" ").map(function(x){o[x]={name:x,val:i++};});return Object.freeze(o);};$.verto.enum={};$.verto.enum.states=Object.freeze({new:{requesting:1,recovering:1,ringing:1,destroy:1,answering:1,hangup:1},requesting:{trying:1,hangup:1,active:1},recovering:{answering:1,hangup:1},trying:{active:1,early:1,hangup:1},ringing:{answering:1,hangup:1},answering:{active:1,hangup:1},active:{answering:1,requesting:1,hangup:1,held:1},held:{hangup:1,active:1},early:{hangup:1,active:1},hangup:{destroy:1},destroy:{},purge:{destroy:1}});$.verto.enum.state=$.verto.ENUM("new requesting trying recovering ringing answering early active held hangup destroy purge");$.verto.enum.direction=$.verto.ENUM("inbound outbound");$.verto.enum.message=$.verto.ENUM("display info pvtEvent");$.verto.enum=Object.freeze($.verto.enum);$.verto.saved=[];$.verto.unloadJobs=[];$(window).bind('beforeunload',function(){for(var f in $.verto.unloadJobs){$.verto.unloadJobs[f]();}
+if($.verto.haltClosure)
+return $.verto.haltClosure();for(var i in $.verto.saved){var verto=$.verto.saved[i];if(verto){verto.purge();verto.logout();}}
+return $.verto.warnOnUnload;});$.verto.videoDevices=[];$.verto.audioInDevices=[];$.verto.audioOutDevices=[];var checkDevices=function(runtime){console.info("enumerating devices");var aud_in=[],aud_out=[],vid=[];var has_video=0,has_audio=0;var Xstream;function gotDevices(deviceInfos){for(var i=0;i!==deviceInfos.length;++i){var deviceInfo=deviceInfos[i];var text="";console.log(deviceInfo);console.log(deviceInfo.kind+": "+deviceInfo.label+" id = "+deviceInfo.deviceId);if(deviceInfo.kind==='audioinput'){text=deviceInfo.label||'microphone '+(aud_in.length+1);aud_in.push({id:deviceInfo.deviceId,kind:"audio_in",label:text});}else if(deviceInfo.kind==='audiooutput'){text=deviceInfo.label||'speaker '+(aud_out.length+1);aud_out.push({id:deviceInfo.deviceId,kind:"audio_out",label:text});}else if(deviceInfo.kind==='videoinput'){text=deviceInfo.label||'camera '+(vid.length+1);vid.push({id:deviceInfo.deviceId,kind:"video",label:text});}else{console.log('Some other kind of source/device: ',deviceInfo);}}
+$.verto.videoDevices=vid;$.verto.audioInDevices=aud_in;$.verto.audioOutDevices=aud_out;console.info("Audio IN Devices",$.verto.audioInDevices);console.info("Audio Out Devices",$.verto.audioOutDevices);console.info("Video Devices",$.verto.videoDevices);if(Xstream){Xstream.getTracks().forEach(function(track){track.stop();});}
+if(runtime){runtime(true);}}
+function handleError(error){console.log('device enumeration error: ',error);if(runtime)runtime(false);}
+function checkTypes(devs){for(var i=0;i!==devs.length;++i){if(devs[i].kind==='audioinput'){has_audio++;}else if(devs[i].kind==='videoinput'){has_video++;}}
+navigator.getUserMedia({audio:(has_audio>0?true:false),video:(has_video>0?true:false)},function(stream){Xstream=stream;navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError);},function(err){console.log("The following error occurred: "+err.name);});}
+navigator.mediaDevices.enumerateDevices().then(checkTypes).catch(handleError);};$.verto.refreshDevices=function(runtime){checkDevices(runtime);}
+$.verto.init=function(obj,runtime){if(!obj){obj={};}
+if(!obj.skipPermCheck&&!obj.skipDeviceCheck){$.FSRTC.checkPerms(function(status){checkDevices(runtime);},true,true);}else if(obj.skipPermCheck&&!obj.skipDeviceCheck){checkDevices(runtime);}else if(!obj.skipPermCheck&&obj.skipDeviceCheck){$.FSRTC.checkPerms(function(status){runtime(status);},true,true);}else{runtime(null);}}
+$.verto.genUUID=function(){return generateGUID();}})(jQuery);(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.adapter=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';var SDPUtils={};SDPUtils.generateIdentifier=function(){return Math.random().toString(36).substr(2,10);};SDPUtils.localCName=SDPUtils.generateIdentifier();SDPUtils.splitLines=function(blob){return blob.trim().split('\n').map(function(line){return line.trim();});};SDPUtils.splitSections=function(blob){var parts=blob.split('\nm=');return parts.map(function(part,index){return(index>0?'m='+part:part).trim()+'\r\n';});};SDPUtils.matchPrefix=function(blob,prefix){return SDPUtils.splitLines(blob).filter(function(line){return line.indexOf(prefix)===0;});};SDPUtils.parseCandidate=function(line){var parts;if(line.indexOf('a=candidate:')===0){parts=line.substring(12).split(' ');}else{parts=line.substring(10).split(' ');}
+var candidate={foundation:parts[0],component:parseInt(parts[1],10),protocol:parts[2].toLowerCase(),priority:parseInt(parts[3],10),ip:parts[4],port:parseInt(parts[5],10),type:parts[7]};for(var i=8;i<parts.length;i+=2){switch(parts[i]){case'raddr':candidate.relatedAddress=parts[i+1];break;case'rport':candidate.relatedPort=parseInt(parts[i+1],10);break;case'tcptype':candidate.tcpType=parts[i+1];break;default:candidate[parts[i]]=parts[i+1];break;}}
+return candidate;};SDPUtils.writeCandidate=function(candidate){var sdp=[];sdp.push(candidate.foundation);sdp.push(candidate.component);sdp.push(candidate.protocol.toUpperCase());sdp.push(candidate.priority);sdp.push(candidate.ip);sdp.push(candidate.port);var type=candidate.type;sdp.push('typ');sdp.push(type);if(type!=='host'&&candidate.relatedAddress&&candidate.relatedPort){sdp.push('raddr');sdp.push(candidate.relatedAddress);sdp.push('rport');sdp.push(candidate.relatedPort);}
+if(candidate.tcpType&&candidate.protocol.toLowerCase()==='tcp'){sdp.push('tcptype');sdp.push(candidate.tcpType);}
+if(candidate.ufrag){sdp.push('ufrag');sdp.push(candidate.ufrag);}
+return'candidate:'+sdp.join(' ');};SDPUtils.parseIceOptions=function(line){return line.substr(14).split(' ');}
+SDPUtils.parseRtpMap=function(line){var parts=line.substr(9).split(' ');var parsed={payloadType:parseInt(parts.shift(),10)};parts=parts[0].split('/');parsed.name=parts[0];parsed.clockRate=parseInt(parts[1],10);parsed.numChannels=parts.length===3?parseInt(parts[2],10):1;return parsed;};SDPUtils.writeRtpMap=function(codec){var pt=codec.payloadType;if(codec.preferredPayloadType!==undefined){pt=codec.preferredPayloadType;}
+return'a=rtpmap:'+pt+' '+codec.name+'/'+codec.clockRate+
+(codec.numChannels!==1?'/'+codec.numChannels:'')+'\r\n';};SDPUtils.parseExtmap=function(line){var parts=line.substr(9).split(' ');return{id:parseInt(parts[0],10),direction:parts[0].indexOf('/')>0?parts[0].split('/')[1]:'sendrecv',uri:parts[1]};};SDPUtils.writeExtmap=function(headerExtension){return'a=extmap:'+(headerExtension.id||headerExtension.preferredId)+
+(headerExtension.direction&&headerExtension.direction!=='sendrecv'?'/'+headerExtension.direction:'')+' '+headerExtension.uri+'\r\n';};SDPUtils.parseFmtp=function(line){var parsed={};var kv;var parts=line.substr(line.indexOf(' ')+1).split(';');for(var j=0;j<parts.length;j++){kv=parts[j].trim().split('=');parsed[kv[0].trim()]=kv[1];}
+return parsed;};SDPUtils.writeFmtp=function(codec){var line='';var pt=codec.payloadType;if(codec.preferredPayloadType!==undefined){pt=codec.preferredPayloadType;}
+if(codec.parameters&&Object.keys(codec.parameters).length){var params=[];Object.keys(codec.parameters).forEach(function(param){params.push(param+'='+codec.parameters[param]);});line+='a=fmtp:'+pt+' '+params.join(';')+'\r\n';}
+return line;};SDPUtils.parseRtcpFb=function(line){var parts=line.substr(line.indexOf(' ')+1).split(' ');return{type:parts.shift(),parameter:parts.join(' ')};};SDPUtils.writeRtcpFb=function(codec){var lines='';var pt=codec.payloadType;if(codec.preferredPayloadType!==undefined){pt=codec.preferredPayloadType;}
+if(codec.rtcpFeedback&&codec.rtcpFeedback.length){codec.rtcpFeedback.forEach(function(fb){lines+='a=rtcp-fb:'+pt+' '+fb.type+
+(fb.parameter&&fb.parameter.length?' '+fb.parameter:'')+'\r\n';});}
+return lines;};SDPUtils.parseSsrcMedia=function(line){var sp=line.indexOf(' ');var parts={ssrc:parseInt(line.substr(7,sp-7),10)};var colon=line.indexOf(':',sp);if(colon>-1){parts.attribute=line.substr(sp+1,colon-sp-1);parts.value=line.substr(colon+1);}else{parts.attribute=line.substr(sp+1);}
+return parts;};SDPUtils.getMid=function(mediaSection){var mid=SDPUtils.matchPrefix(mediaSection,'a=mid:')[0];if(mid){return mid.substr(6);}}
+SDPUtils.parseFingerprint=function(line){var parts=line.substr(14).split(' ');return{algorithm:parts[0].toLowerCase(),value:parts[1]};};SDPUtils.getDtlsParameters=function(mediaSection,sessionpart){var lines=SDPUtils.matchPrefix(mediaSection+sessionpart,'a=fingerprint:');return{role:'auto',fingerprints:lines.map(SDPUtils.parseFingerprint)};};SDPUtils.writeDtlsParameters=function(params,setupType){var sdp='a=setup:'+setupType+'\r\n';params.fingerprints.forEach(function(fp){sdp+='a=fingerprint:'+fp.algorithm+' '+fp.value+'\r\n';});return sdp;};SDPUtils.getIceParameters=function(mediaSection,sessionpart){var lines=SDPUtils.splitLines(mediaSection);lines=lines.concat(SDPUtils.splitLines(sessionpart));var iceParameters={usernameFragment:lines.filter(function(line){return line.indexOf('a=ice-ufrag:')===0;})[0].substr(12),password:lines.filter(function(line){return line.indexOf('a=ice-pwd:')===0;})[0].substr(10)};return iceParameters;};SDPUtils.writeIceParameters=function(params){return'a=ice-ufrag:'+params.usernameFragment+'\r\n'+'a=ice-pwd:'+params.password+'\r\n';};SDPUtils.parseRtpParameters=function(mediaSection){var description={codecs:[],headerExtensions:[],fecMechanisms:[],rtcp:[]};var lines=SDPUtils.splitLines(mediaSection);var mline=lines[0].split(' ');for(var i=3;i<mline.length;i++){var pt=mline[i];var rtpmapline=SDPUtils.matchPrefix(mediaSection,'a=rtpmap:'+pt+' ')[0];if(rtpmapline){var codec=SDPUtils.parseRtpMap(rtpmapline);var fmtps=SDPUtils.matchPrefix(mediaSection,'a=fmtp:'+pt+' ');codec.parameters=fmtps.length?SDPUtils.parseFmtp(fmtps[0]):{};codec.rtcpFeedback=SDPUtils.matchPrefix(mediaSection,'a=rtcp-fb:'+pt+' ').map(SDPUtils.parseRtcpFb);description.codecs.push(codec);switch(codec.name.toUpperCase()){case'RED':case'ULPFEC':description.fecMechanisms.push(codec.name.toUpperCase());break;default:break;}}}
+SDPUtils.matchPrefix(mediaSection,'a=extmap:').forEach(function(line){description.headerExtensions.push(SDPUtils.parseExtmap(line));});return description;};SDPUtils.writeRtpDescription=function(kind,caps){var sdp='';sdp+='m='+kind+' ';sdp+=caps.codecs.length>0?'9':'0';sdp+=' UDP/TLS/RTP/SAVPF ';sdp+=caps.codecs.map(function(codec){if(codec.preferredPayloadType!==undefined){return codec.preferredPayloadType;}
+return codec.payloadType;}).join(' ')+'\r\n';sdp+='c=IN IP4 0.0.0.0\r\n';sdp+='a=rtcp:9 IN IP4 0.0.0.0\r\n';caps.codecs.forEach(function(codec){sdp+=SDPUtils.writeRtpMap(codec);sdp+=SDPUtils.writeFmtp(codec);sdp+=SDPUtils.writeRtcpFb(codec);});var maxptime=0;caps.codecs.forEach(function(codec){if(codec.maxptime>maxptime){maxptime=codec.maxptime;}});if(maxptime>0){sdp+='a=maxptime:'+maxptime+'\r\n';}
+sdp+='a=rtcp-mux\r\n';caps.headerExtensions.forEach(function(extension){sdp+=SDPUtils.writeExtmap(extension);});return sdp;};SDPUtils.parseRtpEncodingParameters=function(mediaSection){var encodingParameters=[];var description=SDPUtils.parseRtpParameters(mediaSection);var hasRed=description.fecMechanisms.indexOf('RED')!==-1;var hasUlpfec=description.fecMechanisms.indexOf('ULPFEC')!==-1;var ssrcs=SDPUtils.matchPrefix(mediaSection,'a=ssrc:').map(function(line){return SDPUtils.parseSsrcMedia(line);}).filter(function(parts){return parts.attribute==='cname';});var primarySsrc=ssrcs.length>0&&ssrcs[0].ssrc;var secondarySsrc;var flows=SDPUtils.matchPrefix(mediaSection,'a=ssrc-group:FID').map(function(line){var parts=line.split(' ');parts.shift();return parts.map(function(part){return parseInt(part,10);});});if(flows.length>0&&flows[0].length>1&&flows[0][0]===primarySsrc){secondarySsrc=flows[0][1];}
+description.codecs.forEach(function(codec){if(codec.name.toUpperCase()==='RTX'&&codec.parameters.apt){var encParam={ssrc:primarySsrc,codecPayloadType:parseInt(codec.parameters.apt,10),rtx:{ssrc:secondarySsrc}};encodingParameters.push(encParam);if(hasRed){encParam=JSON.parse(JSON.stringify(encParam));encParam.fec={ssrc:secondarySsrc,mechanism:hasUlpfec?'red+ulpfec':'red'};encodingParameters.push(encParam);}}});if(encodingParameters.length===0&&primarySsrc){encodingParameters.push({ssrc:primarySsrc});}
+var bandwidth=SDPUtils.matchPrefix(mediaSection,'b=');if(bandwidth.length){if(bandwidth[0].indexOf('b=TIAS:')===0){bandwidth=parseInt(bandwidth[0].substr(7),10);}else if(bandwidth[0].indexOf('b=AS:')===0){bandwidth=parseInt(bandwidth[0].substr(5),10)*1000*0.95
+-(50*40*8);}else{bandwidth=undefined;}
+encodingParameters.forEach(function(params){params.maxBitrate=bandwidth;});}
+return encodingParameters;};SDPUtils.parseRtcpParameters=function(mediaSection){var rtcpParameters={};var cname;var remoteSsrc=SDPUtils.matchPrefix(mediaSection,'a=ssrc:').map(function(line){return SDPUtils.parseSsrcMedia(line);}).filter(function(obj){return obj.attribute==='cname';})[0];if(remoteSsrc){rtcpParameters.cname=remoteSsrc.value;rtcpParameters.ssrc=remoteSsrc.ssrc;}
+var rsize=SDPUtils.matchPrefix(mediaSection,'a=rtcp-rsize');rtcpParameters.reducedSize=rsize.length>0;rtcpParameters.compound=rsize.length===0;var mux=SDPUtils.matchPrefix(mediaSection,'a=rtcp-mux');rtcpParameters.mux=mux.length>0;return rtcpParameters;};SDPUtils.parseMsid=function(mediaSection){var parts;var spec=SDPUtils.matchPrefix(mediaSection,'a=msid:');if(spec.length===1){parts=spec[0].substr(7).split(' ');return{stream:parts[0],track:parts[1]};}
+var planB=SDPUtils.matchPrefix(mediaSection,'a=ssrc:').map(function(line){return SDPUtils.parseSsrcMedia(line);}).filter(function(parts){return parts.attribute==='msid';});if(planB.length>0){parts=planB[0].value.split(' ');return{stream:parts[0],track:parts[1]};}};SDPUtils.generateSessionId=function(){return Math.random().toString().substr(2,21);};SDPUtils.writeSessionBoilerplate=function(sessId){var sessionId;if(sessId){sessionId=sessId;}else{sessionId=SDPUtils.generateSessionId();}
+return'v=0\r\n'+'o=thisisadapterortc '+sessionId+' 2 IN IP4 127.0.0.1\r\n'+'s=-\r\n'+'t=0 0\r\n';};SDPUtils.writeMediaSection=function(transceiver,caps,type,stream){var sdp=SDPUtils.writeRtpDescription(transceiver.kind,caps);sdp+=SDPUtils.writeIceParameters(transceiver.iceGatherer.getLocalParameters());sdp+=SDPUtils.writeDtlsParameters(transceiver.dtlsTransport.getLocalParameters(),type==='offer'?'actpass':'active');sdp+='a=mid:'+transceiver.mid+'\r\n';if(transceiver.direction){sdp+='a='+transceiver.direction+'\r\n';}else if(transceiver.rtpSender&&transceiver.rtpReceiver){sdp+='a=sendrecv\r\n';}else if(transceiver.rtpSender){sdp+='a=sendonly\r\n';}else if(transceiver.rtpReceiver){sdp+='a=recvonly\r\n';}else{sdp+='a=inactive\r\n';}
+if(transceiver.rtpSender){var msid='msid:'+stream.id+' '+
+transceiver.rtpSender.track.id+'\r\n';sdp+='a='+msid;sdp+='a=ssrc:'+transceiver.sendEncodingParameters[0].ssrc+' '+msid;if(transceiver.sendEncodingParameters[0].rtx){sdp+='a=ssrc:'+transceiver.sendEncodingParameters[0].rtx.ssrc+' '+msid;sdp+='a=ssrc-group:FID '+
+transceiver.sendEncodingParameters[0].ssrc+' '+
+transceiver.sendEncodingParameters[0].rtx.ssrc+'\r\n';}}
+sdp+='a=ssrc:'+transceiver.sendEncodingParameters[0].ssrc+' cname:'+SDPUtils.localCName+'\r\n';if(transceiver.rtpSender&&transceiver.sendEncodingParameters[0].rtx){sdp+='a=ssrc:'+transceiver.sendEncodingParameters[0].rtx.ssrc+' cname:'+SDPUtils.localCName+'\r\n';}
+return sdp;};SDPUtils.getDirection=function(mediaSection,sessionpart){var lines=SDPUtils.splitLines(mediaSection);for(var i=0;i<lines.length;i++){switch(lines[i]){case'a=sendrecv':case'a=sendonly':case'a=recvonly':case'a=inactive':return lines[i].substr(2);default:}}
+if(sessionpart){return SDPUtils.getDirection(sessionpart);}
+return'sendrecv';};SDPUtils.getKind=function(mediaSection){var lines=SDPUtils.splitLines(mediaSection);var mline=lines[0].split(' ');return mline[0].substr(2);};SDPUtils.isRejected=function(mediaSection){return mediaSection.split(' ',2)[1]==='0';};module.exports=SDPUtils;},{}],2:[function(require,module,exports){(function(global){'use strict';var adapterFactory=require('./adapter_factory.js');module.exports=adapterFactory({window:global.window});}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./adapter_factory.js":3}],3:[function(require,module,exports){'use strict';module.exports=function(dependencies){var window=dependencies&&dependencies.window;var utils=require('./utils');var logging=utils.log;var browserDetails=utils.detectBrowser(window);var adapter={browserDetails:browserDetails,extractVersion:utils.extractVersion,disableLog:utils.disableLog,disableWarnings:utils.disableWarnings};var chromeShim=require('./chrome/chrome_shim')||null;var edgeShim=require('./edge/edge_shim')||null;var firefoxShim=require('./firefox/firefox_shim')||null;var safariShim=require('./safari/safari_shim')||null;switch(browserDetails.browser){case'chrome':if(!chromeShim||!chromeShim.shimPeerConnection){logging('Chrome shim is not included in this adapter release.');return adapter;}
+logging('adapter.js shimming chrome.');adapter.browserShim=chromeShim;chromeShim.shimGetUserMedia(window);chromeShim.shimMediaStream(window);utils.shimCreateObjectURL(window);chromeShim.shimSourceObject(window);chromeShim.shimPeerConnection(window);chromeShim.shimOnTrack(window);chromeShim.shimGetSendersWithDtmf(window);break;case'firefox':if(!firefoxShim||!firefoxShim.shimPeerConnection){logging('Firefox shim is not included in this adapter release.');return adapter;}
+logging('adapter.js shimming firefox.');adapter.browserShim=firefoxShim;firefoxShim.shimGetUserMedia(window);utils.shimCreateObjectURL(window);firefoxShim.shimSourceObject(window);firefoxShim.shimPeerConnection(window);firefoxShim.shimOnTrack(window);break;case'edge':if(!edgeShim||!edgeShim.shimPeerConnection){logging('MS edge shim is not included in this adapter release.');return adapter;}
+logging('adapter.js shimming edge.');adapter.browserShim=edgeShim;edgeShim.shimGetUserMedia(window);utils.shimCreateObjectURL(window);edgeShim.shimPeerConnection(window);edgeShim.shimReplaceTrack(window);break;case'safari':if(!safariShim){logging('Safari shim is not included in this adapter release.');return adapter;}
+logging('adapter.js shimming safari.');adapter.browserShim=safariShim;utils.shimCreateObjectURL(window);safariShim.shimRTCIceServerUrls(window);safariShim.shimCallbacksAPI(window);safariShim.shimLocalStreamsAPI(window);safariShim.shimRemoteStreamsAPI(window);safariShim.shimGetUserMedia(window);break;default:logging('Unsupported browser!');break;}
+return adapter;};},{"./chrome/chrome_shim":4,"./edge/edge_shim":6,"./firefox/firefox_shim":9,"./safari/safari_shim":11,"./utils":12}],4:[function(require,module,exports){'use strict';var utils=require('../utils.js');var logging=utils.log;var chromeShim={shimMediaStream:function(window){window.MediaStream=window.MediaStream||window.webkitMediaStream;},shimOnTrack:function(window){if(typeof window==='object'&&window.RTCPeerConnection&&!('ontrack'in
+window.RTCPeerConnection.prototype)){Object.defineProperty(window.RTCPeerConnection.prototype,'ontrack',{get:function(){return this._ontrack;},set:function(f){var self=this;if(this._ontrack){this.removeEventListener('track',this._ontrack);this.removeEventListener('addstream',this._ontrackpoly);}
+this.addEventListener('track',this._ontrack=f);this.addEventListener('addstream',this._ontrackpoly=function(e){e.stream.addEventListener('addtrack',function(te){var receiver;if(window.RTCPeerConnection.prototype.getReceivers){receiver=self.getReceivers().find(function(r){return r.track.id===te.track.id;});}else{receiver={track:te.track};}
+var event=new Event('track');event.track=te.track;event.receiver=receiver;event.streams=[e.stream];self.dispatchEvent(event);});e.stream.getTracks().forEach(function(track){var receiver;if(window.RTCPeerConnection.prototype.getReceivers){receiver=self.getReceivers().find(function(r){return r.track.id===track.id;});}else{receiver={track:track};}
+var event=new Event('track');event.track=track;event.receiver=receiver;event.streams=[e.stream];this.dispatchEvent(event);}.bind(this));}.bind(this));}});}},shimGetSendersWithDtmf:function(window){if(typeof window==='object'&&window.RTCPeerConnection&&!('getSenders'in window.RTCPeerConnection.prototype)&&'createDTMFSender'in window.RTCPeerConnection.prototype){window.RTCPeerConnection.prototype.getSenders=function(){return this._senders||[];};var origAddStream=window.RTCPeerConnection.prototype.addStream;var origRemoveStream=window.RTCPeerConnection.prototype.removeStream;if(!window.RTCPeerConnection.prototype.addTrack){window.RTCPeerConnection.prototype.addTrack=function(track,stream){var pc=this;if(pc.signalingState==='closed'){throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.','InvalidStateError');}
+var streams=[].slice.call(arguments,1);if(streams.length!==1||!streams[0].getTracks().find(function(t){return t===track;})){throw new DOMException('The adapter.js addTrack polyfill only supports a single '+' stream which is associated with the specified track.','NotSupportedError');}
+pc._senders=pc._senders||[];var alreadyExists=pc._senders.find(function(t){return t.track===track;});if(alreadyExists){throw new DOMException('Track already exists.','InvalidAccessError');}
+pc._streams=pc._streams||{};var oldStream=pc._streams[stream.id];if(oldStream){oldStream.addTrack(track);pc.removeStream(oldStream);pc.addStream(oldStream);}else{var newStream=new window.MediaStream([track]);pc._streams[stream.id]=newStream;pc.addStream(newStream);}
+var sender={track:track,get dtmf(){if(this._dtmf===undefined){if(track.kind==='audio'){this._dtmf=pc.createDTMFSender(track);}else{this._dtmf=null;}}
+return this._dtmf;}};pc._senders.push(sender);return sender;};}
+window.RTCPeerConnection.prototype.addStream=function(stream){var pc=this;pc._senders=pc._senders||[];origAddStream.apply(pc,[stream]);stream.getTracks().forEach(function(track){pc._senders.push({track:track,get dtmf(){if(this._dtmf===undefined){if(track.kind==='audio'){this._dtmf=pc.createDTMFSender(track);}else{this._dtmf=null;}}
+return this._dtmf;}});});};window.RTCPeerConnection.prototype.removeStream=function(stream){var pc=this;pc._senders=pc._senders||[];origRemoveStream.apply(pc,[stream]);stream.getTracks().forEach(function(track){var sender=pc._senders.find(function(s){return s.track===track;});if(sender){pc._senders.splice(pc._senders.indexOf(sender),1);}});};}else if(typeof window==='object'&&window.RTCPeerConnection&&'getSenders'in window.RTCPeerConnection.prototype&&'createDTMFSender'in window.RTCPeerConnection.prototype&&window.RTCRtpSender&&!('dtmf'in window.RTCRtpSender.prototype)){var origGetSenders=window.RTCPeerConnection.prototype.getSenders;window.RTCPeerConnection.prototype.getSenders=function(){var pc=this;var senders=origGetSenders.apply(pc,[]);senders.forEach(function(sender){sender._pc=pc;});return senders;};Object.defineProperty(window.RTCRtpSender.prototype,'dtmf',{get:function(){if(this._dtmf===undefined){if(this.track.kind==='audio'){this._dtmf=this._pc.createDTMFSender(this.track);}else{this._dtmf=null;}}
+return this._dtmf;},});}},shimSourceObject:function(window){var URL=window&&window.URL;if(typeof window==='object'){if(window.HTMLMediaElement&&!('srcObject'in window.HTMLMediaElement.prototype)){Object.defineProperty(window.HTMLMediaElement.prototype,'srcObject',{get:function(){return this._srcObject;},set:function(stream){var self=this;this._srcObject=stream;if(this.src){URL.revokeObjectURL(this.src);}
+if(!stream){this.src='';return undefined;}
+this.src=URL.createObjectURL(stream);stream.addEventListener('addtrack',function(){if(self.src){URL.revokeObjectURL(self.src);}
+self.src=URL.createObjectURL(stream);});stream.addEventListener('removetrack',function(){if(self.src){URL.revokeObjectURL(self.src);}
+self.src=URL.createObjectURL(stream);});}});}}},shimPeerConnection:function(window){var browserDetails=utils.detectBrowser(window);if(!window.RTCPeerConnection){window.RTCPeerConnection=function(pcConfig,pcConstraints){logging('PeerConnection');if(pcConfig&&pcConfig.iceTransportPolicy){pcConfig.iceTransports=pcConfig.iceTransportPolicy;}
+return new window.webkitRTCPeerConnection(pcConfig,pcConstraints);};window.RTCPeerConnection.prototype=window.webkitRTCPeerConnection.prototype;if(window.webkitRTCPeerConnection.generateCertificate){Object.defineProperty(window.RTCPeerConnection,'generateCertificate',{get:function(){return window.webkitRTCPeerConnection.generateCertificate;}});}}else{var OrigPeerConnection=window.RTCPeerConnection;window.RTCPeerConnection=function(pcConfig,pcConstraints){if(pcConfig&&pcConfig.iceServers){var newIceServers=[];for(var i=0;i<pcConfig.iceServers.length;i++){var server=pcConfig.iceServers[i];if(!server.hasOwnProperty('urls')&&server.hasOwnProperty('url')){console.warn('RTCIceServer.url is deprecated! Use urls instead.');server=JSON.parse(JSON.stringify(server));server.urls=server.url;newIceServers.push(server);}else{newIceServers.push(pcConfig.iceServers[i]);}}
+pcConfig.iceServers=newIceServers;}
+return new OrigPeerConnection(pcConfig,pcConstraints);};window.RTCPeerConnection.prototype=OrigPeerConnection.prototype;Object.defineProperty(window.RTCPeerConnection,'generateCertificate',{get:function(){return OrigPeerConnection.generateCertificate;}});}
+var origGetStats=window.RTCPeerConnection.prototype.getStats;window.RTCPeerConnection.prototype.getStats=function(selector,successCallback,errorCallback){var self=this;var args=arguments;if(arguments.length>0&&typeof selector==='function'){return origGetStats.apply(this,arguments);}
+if(origGetStats.length===0&&(arguments.length===0||typeof arguments[0]!=='function')){return origGetStats.apply(this,[]);}
+var fixChromeStats_=function(response){var standardReport={};var reports=response.result();reports.forEach(function(report){var standardStats={id:report.id,timestamp:report.timestamp,type:{localcandidate:'local-candidate',remotecandidate:'remote-candidate'}[report.type]||report.type};report.names().forEach(function(name){standardStats[name]=report.stat(name);});standardReport[standardStats.id]=standardStats;});return standardReport;};var makeMapStats=function(stats){return new Map(Object.keys(stats).map(function(key){return[key,stats[key]];}));};if(arguments.length>=2){var successCallbackWrapper_=function(response){args[1](makeMapStats(fixChromeStats_(response)));};return origGetStats.apply(this,[successCallbackWrapper_,arguments[0]]);}
+return new Promise(function(resolve,reject){origGetStats.apply(self,[function(response){resolve(makeMapStats(fixChromeStats_(response)));},reject]);}).then(successCallback,errorCallback);};if(browserDetails.version<51){['setLocalDescription','setRemoteDescription','addIceCandidate'].forEach(function(method){var nativeMethod=window.RTCPeerConnection.prototype[method];window.RTCPeerConnection.prototype[method]=function(){var args=arguments;var self=this;var promise=new Promise(function(resolve,reject){nativeMethod.apply(self,[args[0],resolve,reject]);});if(args.length<2){return promise;}
+return promise.then(function(){args[1].apply(null,[]);},function(err){if(args.length>=3){args[2].apply(null,[err]);}});};});}
+if(browserDetails.version<52){['createOffer','createAnswer'].forEach(function(method){var nativeMethod=window.RTCPeerConnection.prototype[method];window.RTCPeerConnection.prototype[method]=function(){var self=this;if(arguments.length<1||(arguments.length===1&&typeof arguments[0]==='object')){var opts=arguments.length===1?arguments[0]:undefined;return new Promise(function(resolve,reject){nativeMethod.apply(self,[resolve,reject,opts]);});}
+return nativeMethod.apply(this,arguments);};});}
+['setLocalDescription','setRemoteDescription','addIceCandidate'].forEach(function(method){var nativeMethod=window.RTCPeerConnection.prototype[method];window.RTCPeerConnection.prototype[method]=function(){arguments[0]=new((method==='addIceCandidate')?window.RTCIceCandidate:window.RTCSessionDescription)(arguments[0]);return nativeMethod.apply(this,arguments);};});var nativeAddIceCandidate=window.RTCPeerConnection.prototype.addIceCandidate;window.RTCPeerConnection.prototype.addIceCandidate=function(){if(!arguments[0]){if(arguments[1]){arguments[1].apply(null);}
+return Promise.resolve();}
+return nativeAddIceCandidate.apply(this,arguments);};}};module.exports={shimMediaStream:chromeShim.shimMediaStream,shimOnTrack:chromeShim.shimOnTrack,shimGetSendersWithDtmf:chromeShim.shimGetSendersWithDtmf,shimSourceObject:chromeShim.shimSourceObject,shimPeerConnection:chromeShim.shimPeerConnection,shimGetUserMedia:require('./getusermedia')};},{"../utils.js":12,"./getusermedia":5}],5:[function(require,module,exports){'use strict';var utils=require('../utils.js');var logging=utils.log;module.exports=function(window){var browserDetails=utils.detectBrowser(window);var navigator=window&&window.navigator;var constraintsToChrome_=function(c){if(typeof c!=='object'||c.mandatory||c.optional){return c;}
+var cc={};Object.keys(c).forEach(function(key){if(key==='require'||key==='advanced'||key==='mediaSource'){return;}
+var r=(typeof c[key]==='object')?c[key]:{ideal:c[key]};if(r.exact!==undefined&&typeof r.exact==='number'){r.min=r.max=r.exact;}
+var oldname_=function(prefix,name){if(prefix){return prefix+name.charAt(0).toUpperCase()+name.slice(1);}
+return(name==='deviceId')?'sourceId':name;};if(r.ideal!==undefined){cc.optional=cc.optional||[];var oc={};if(typeof r.ideal==='number'){oc[oldname_('min',key)]=r.ideal;cc.optional.push(oc);oc={};oc[oldname_('max',key)]=r.ideal;cc.optional.push(oc);}else{oc[oldname_('',key)]=r.ideal;cc.optional.push(oc);}}
+if(r.exact!==undefined&&typeof r.exact!=='number'){cc.mandatory=cc.mandatory||{};cc.mandatory[oldname_('',key)]=r.exact;}else{['min','max'].forEach(function(mix){if(r[mix]!==undefined){cc.mandatory=cc.mandatory||{};cc.mandatory[oldname_(mix,key)]=r[mix];}});}});if(c.advanced){cc.optional=(cc.optional||[]).concat(c.advanced);}
+return cc;};var shimConstraints_=function(constraints,func){constraints=JSON.parse(JSON.stringify(constraints));if(constraints&&typeof constraints.audio==='object'){var remap=function(obj,a,b){if(a in obj&&!(b in obj)){obj[b]=obj[a];delete obj[a];}};constraints=JSON.parse(JSON.stringify(constraints));remap(constraints.audio,'autoGainControl','googAutoGainControl');remap(constraints.audio,'noiseSuppression','googNoiseSuppression');constraints.audio=constraintsToChrome_(constraints.audio);}
+if(constraints&&typeof constraints.video==='object'){var face=constraints.video.facingMode;face=face&&((typeof face==='object')?face:{ideal:face});var getSupportedFacingModeLies=browserDetails.version<61;if((face&&(face.exact==='user'||face.exact==='environment'||face.ideal==='user'||face.ideal==='environment'))&&!(navigator.mediaDevices.getSupportedConstraints&&navigator.mediaDevices.getSupportedConstraints().facingMode&&!getSupportedFacingModeLies)){delete constraints.video.facingMode;var matches;if(face.exact==='environment'||face.ideal==='environment'){matches=['back','rear'];}else if(face.exact==='user'||face.ideal==='user'){matches=['front'];}
+if(matches){return navigator.mediaDevices.enumerateDevices().then(function(devices){devices=devices.filter(function(d){return d.kind==='videoinput';});var dev=devices.find(function(d){return matches.some(function(match){return d.label.toLowerCase().indexOf(match)!==-1;});});if(!dev&&devices.length&&matches.indexOf('back')!==-1){dev=devices[devices.length-1];}
+if(dev){constraints.video.deviceId=face.exact?{exact:dev.deviceId}:{ideal:dev.deviceId};}
+constraints.video=constraintsToChrome_(constraints.video);logging('chrome: '+JSON.stringify(constraints));return func(constraints);});}}
+constraints.video=constraintsToChrome_(constraints.video);}
+logging('chrome: '+JSON.stringify(constraints));return func(constraints);};var shimError_=function(e){return{name:{PermissionDeniedError:'NotAllowedError',InvalidStateError:'NotReadableError',DevicesNotFoundError:'NotFoundError',ConstraintNotSatisfiedError:'OverconstrainedError',TrackStartError:'NotReadableError',MediaDeviceFailedDueToShutdown:'NotReadableError',MediaDeviceKillSwitchOn:'NotReadableError'}[e.name]||e.name,message:e.message,constraint:e.constraintName,toString:function(){return this.name+(this.message&&': ')+this.message;}};};var getUserMedia_=function(constraints,onSuccess,onError){shimConstraints_(constraints,function(c){navigator.webkitGetUserMedia(c,onSuccess,function(e){onError(shimError_(e));});});};navigator.getUserMedia=getUserMedia_;var getUserMediaPromise_=function(constraints){return new Promise(function(resolve,reject){navigator.getUserMedia(constraints,resolve,reject);});};if(!navigator.mediaDevices){navigator.mediaDevices={getUserMedia:getUserMediaPromise_,enumerateDevices:function(){return new Promise(function(resolve){var kinds={audio:'audioinput',video:'videoinput'};return window.MediaStreamTrack.getSources(function(devices){resolve(devices.map(function(device){return{label:device.label,kind:kinds[device.kind],deviceId:device.id,groupId:''};}));});});},getSupportedConstraints:function(){return{deviceId:true,echoCancellation:true,facingMode:true,frameRate:true,height:true,width:true};}};}
+if(!navigator.mediaDevices.getUserMedia){navigator.mediaDevices.getUserMedia=function(constraints){return getUserMediaPromise_(constraints);};}else{var origGetUserMedia=navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);navigator.mediaDevices.getUserMedia=function(cs){return shimConstraints_(cs,function(c){return origGetUserMedia(c).then(function(stream){if(c.audio&&!stream.getAudioTracks().length||c.video&&!stream.getVideoTracks().length){stream.getTracks().forEach(function(track){track.stop();});throw new DOMException('','NotFoundError');}
+return stream;},function(e){return Promise.reject(shimError_(e));});});};}
+if(typeof navigator.mediaDevices.addEventListener==='undefined'){navigator.mediaDevices.addEventListener=function(){logging('Dummy mediaDevices.addEventListener called.');};}
+if(typeof navigator.mediaDevices.removeEventListener==='undefined'){navigator.mediaDevices.removeEventListener=function(){logging('Dummy mediaDevices.removeEventListener called.');};}};},{"../utils.js":12}],6:[function(require,module,exports){'use strict';var utils=require('../utils');var shimRTCPeerConnection=require('./rtcpeerconnection_shim');module.exports={shimGetUserMedia:require('./getusermedia'),shimPeerConnection:function(window){var browserDetails=utils.detectBrowser(window);if(window.RTCIceGatherer){if(!window.RTCIceCandidate){window.RTCIceCandidate=function(args){return args;};}
+if(!window.RTCSessionDescription){window.RTCSessionDescription=function(args){return args;};}
+if(browserDetails.version<15025){var origMSTEnabled=Object.getOwnPropertyDescriptor(window.MediaStreamTrack.prototype,'enabled');Object.defineProperty(window.MediaStreamTrack.prototype,'enabled',{set:function(value){origMSTEnabled.set.call(this,value);var ev=new Event('enabled');ev.enabled=value;this.dispatchEvent(ev);}});}}
+window.RTCPeerConnection=shimRTCPeerConnection(window,browserDetails.version);},shimReplaceTrack:function(window){if(window.RTCRtpSender&&!('replaceTrack'in window.RTCRtpSender.prototype)){window.RTCRtpSender.prototype.replaceTrack=window.RTCRtpSender.prototype.setTrack;}}};},{"../utils":12,"./getusermedia":7,"./rtcpeerconnection_shim":8}],7:[function(require,module,exports){'use strict';module.exports=function(window){var navigator=window&&window.navigator;var shimError_=function(e){return{name:{PermissionDeniedError:'NotAllowedError'}[e.name]||e.name,message:e.message,constraint:e.constraint,toString:function(){return this.name;}};};var origGetUserMedia=navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);navigator.mediaDevices.getUserMedia=function(c){return origGetUserMedia(c).catch(function(e){return Promise.reject(shimError_(e));});};};},{}],8:[function(require,module,exports){'use strict';var SDPUtils=require('sdp');function sortTracks(tracks){var audioTracks=tracks.filter(function(track){return track.kind==='audio';});var videoTracks=tracks.filter(function(track){return track.kind==='video';});tracks=[];while(audioTracks.length||videoTracks.length){if(audioTracks.length){tracks.push(audioTracks.shift());}
+if(videoTracks.length){tracks.push(videoTracks.shift());}}
+return tracks;}
+function filterIceServers(iceServers,edgeVersion){var hasTurn=false;iceServers=JSON.parse(JSON.stringify(iceServers));return iceServers.filter(function(server){if(server&&(server.urls||server.url)){var urls=server.urls||server.url;if(server.url&&!server.urls){console.warn('RTCIceServer.url is deprecated! Use urls instead.');}
+var isString=typeof urls==='string';if(isString){urls=[urls];}
+urls=urls.filter(function(url){var validTurn=url.indexOf('turn:')===0&&url.indexOf('transport=udp')!==-1&&url.indexOf('turn:[')===-1&&!hasTurn;if(validTurn){hasTurn=true;return true;}
+return url.indexOf('stun:')===0&&edgeVersion>=14393;});delete server.url;server.urls=isString?urls[0]:urls;return!!urls.length;}
+return false;});}
+function getCommonCapabilities(localCapabilities,remoteCapabilities){var commonCapabilities={codecs:[],headerExtensions:[],fecMechanisms:[]};var findCodecByPayloadType=function(pt,codecs){pt=parseInt(pt,10);for(var i=0;i<codecs.length;i++){if(codecs[i].payloadType===pt||codecs[i].preferredPayloadType===pt){return codecs[i];}}};var rtxCapabilityMatches=function(lRtx,rRtx,lCodecs,rCodecs){var lCodec=findCodecByPayloadType(lRtx.parameters.apt,lCodecs);var rCodec=findCodecByPayloadType(rRtx.parameters.apt,rCodecs);return lCodec&&rCodec&&lCodec.name.toLowerCase()===rCodec.name.toLowerCase();};localCapabilities.codecs.forEach(function(lCodec){for(var i=0;i<remoteCapabilities.codecs.length;i++){var rCodec=remoteCapabilities.codecs[i];if(lCodec.name.toLowerCase()===rCodec.name.toLowerCase()&&lCodec.clockRate===rCodec.clockRate){if(lCodec.name.toLowerCase()==='rtx'&&lCodec.parameters&&rCodec.parameters.apt){if(!rtxCapabilityMatches(lCodec,rCodec,localCapabilities.codecs,remoteCapabilities.codecs)){continue;}}
+rCodec=JSON.parse(JSON.stringify(rCodec));rCodec.numChannels=Math.min(lCodec.numChannels,rCodec.numChannels);commonCapabilities.codecs.push(rCodec);rCodec.rtcpFeedback=rCodec.rtcpFeedback.filter(function(fb){for(var j=0;j<lCodec.rtcpFeedback.length;j++){if(lCodec.rtcpFeedback[j].type===fb.type&&lCodec.rtcpFeedback[j].parameter===fb.parameter){return true;}}
+return false;});break;}}});localCapabilities.headerExtensions.forEach(function(lHeaderExtension){for(var i=0;i<remoteCapabilities.headerExtensions.length;i++){var rHeaderExtension=remoteCapabilities.headerExtensions[i];if(lHeaderExtension.uri===rHeaderExtension.uri){commonCapabilities.headerExtensions.push(rHeaderExtension);break;}}});return commonCapabilities;}
+function isActionAllowedInSignalingState(action,type,signalingState){return{offer:{setLocalDescription:['stable','have-local-offer'],setRemoteDescription:['stable','have-remote-offer']},answer:{setLocalDescription:['have-remote-offer','have-local-pranswer'],setRemoteDescription:['have-local-offer','have-remote-pranswer']}}[type][action].indexOf(signalingState)!==-1;}
+module.exports=function(window,edgeVersion){var RTCPeerConnection=function(config){var self=this;var _eventTarget=document.createDocumentFragment();['addEventListener','removeEventListener','dispatchEvent'].forEach(function(method){self[method]=_eventTarget[method].bind(_eventTarget);});this.needNegotiation=false;this.onicecandidate=null;this.onaddstream=null;this.ontrack=null;this.onremovestream=null;this.onsignalingstatechange=null;this.oniceconnectionstatechange=null;this.onicegatheringstatechange=null;this.onnegotiationneeded=null;this.ondatachannel=null;this.canTrickleIceCandidates=null;this.localStreams=[];this.remoteStreams=[];this.getLocalStreams=function(){return self.localStreams;};this.getRemoteStreams=function(){return self.remoteStreams;};this.localDescription=new window.RTCSessionDescription({type:'',sdp:''});this.remoteDescription=new window.RTCSessionDescription({type:'',sdp:''});this.signalingState='stable';this.iceConnectionState='new';this.iceGatheringState='new';this.iceOptions={gatherPolicy:'all',iceServers:[]};if(config&&config.iceTransportPolicy){switch(config.iceTransportPolicy){case'all':case'relay':this.iceOptions.gatherPolicy=config.iceTransportPolicy;break;default:break;}}
+this.usingBundle=config&&config.bundlePolicy==='max-bundle';if(config&&config.iceServers){this.iceOptions.iceServers=filterIceServers(config.iceServers,edgeVersion);}
+this._config=config||{};this.transceivers=[];this._localIceCandidatesBuffer=[];this._sdpSessionId=SDPUtils.generateSessionId();};RTCPeerConnection.prototype._emitGatheringStateChange=function(){var event=new Event('icegatheringstatechange');this.dispatchEvent(event);if(this.onicegatheringstatechange!==null){this.onicegatheringstatechange(event);}};RTCPeerConnection.prototype._emitBufferedCandidates=function(){var self=this;var sections=SDPUtils.splitSections(self.localDescription.sdp);this._localIceCandidatesBuffer.forEach(function(event){var end=!event.candidate||Object.keys(event.candidate).length===0;if(end){for(var j=1;j<sections.length;j++){if(sections[j].indexOf('\r\na=end-of-candidates\r\n')===-1){sections[j]+='a=end-of-candidates\r\n';}}}else{sections[event.candidate.sdpMLineIndex+1]+='a='+event.candidate.candidate+'\r\n';}
+self.localDescription.sdp=sections.join('');self.dispatchEvent(event);if(self.onicecandidate!==null){self.onicecandidate(event);}
+if(!event.candidate&&self.iceGatheringState!=='complete'){var complete=self.transceivers.every(function(transceiver){return transceiver.iceGatherer&&transceiver.iceGatherer.state==='completed';});if(complete&&self.iceGatheringStateChange!=='complete'){self.iceGatheringState='complete';self._emitGatheringStateChange();}}});this._localIceCandidatesBuffer=[];};RTCPeerConnection.prototype.getConfiguration=function(){return this._config;};RTCPeerConnection.prototype._createTransceiver=function(kind){var hasBundleTransport=this.transceivers.length>0;var transceiver={track:null,iceGatherer:null,iceTransport:null,dtlsTransport:null,localCapabilities:null,remoteCapabilities:null,rtpSender:null,rtpReceiver:null,kind:kind,mid:null,sendEncodingParameters:null,recvEncodingParameters:null,stream:null,wantReceive:true};if(this.usingBundle&&hasBundleTransport){transceiver.iceTransport=this.transceivers[0].iceTransport;transceiver.dtlsTransport=this.transceivers[0].dtlsTransport;}else{var transports=this._createIceAndDtlsTransports();transceiver.iceTransport=transports.iceTransport;transceiver.dtlsTransport=transports.dtlsTransport;}
+this.transceivers.push(transceiver);return transceiver;};RTCPeerConnection.prototype.addTrack=function(track,stream){var transceiver;for(var i=0;i<this.transceivers.length;i++){if(!this.transceivers[i].track&&this.transceivers[i].kind===track.kind){transceiver=this.transceivers[i];}}
+if(!transceiver){transceiver=this._createTransceiver(track.kind);}
+transceiver.track=track;transceiver.stream=stream;transceiver.rtpSender=new window.RTCRtpSender(track,transceiver.dtlsTransport);this._maybeFireNegotiationNeeded();return transceiver.rtpSender;};RTCPeerConnection.prototype.addStream=function(stream){var self=this;if(edgeVersion>=15025){this.localStreams.push(stream);stream.getTracks().forEach(function(track){self.addTrack(track,stream);});}else{var clonedStream=stream.clone();stream.getTracks().forEach(function(track,idx){var clonedTrack=clonedStream.getTracks()[idx];track.addEventListener('enabled',function(event){clonedTrack.enabled=event.enabled;});});clonedStream.getTracks().forEach(function(track){self.addTrack(track,clonedStream);});this.localStreams.push(clonedStream);}
+this._maybeFireNegotiationNeeded();};RTCPeerConnection.prototype.removeStream=function(stream){var idx=this.localStreams.indexOf(stream);if(idx>-1){this.localStreams.splice(idx,1);this._maybeFireNegotiationNeeded();}};RTCPeerConnection.prototype.getSenders=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpSender;}).map(function(transceiver){return transceiver.rtpSender;});};RTCPeerConnection.prototype.getReceivers=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpReceiver;}).map(function(transceiver){return transceiver.rtpReceiver;});};RTCPeerConnection.prototype._createIceGatherer=function(mid,sdpMLineIndex){var self=this;var iceGatherer=new window.RTCIceGatherer(self.iceOptions);iceGatherer.onlocalcandidate=function(evt){var event=new Event('icecandidate');event.candidate={sdpMid:mid,sdpMLineIndex:sdpMLineIndex};var cand=evt.candidate;var end=!cand||Object.keys(cand).length===0;if(end){if(iceGatherer.state===undefined){iceGatherer.state='completed';}}else{cand.component=1;event.candidate.candidate=SDPUtils.writeCandidate(cand);}
+var sections=SDPUtils.splitSections(self.localDescription.sdp);if(!end){sections[event.candidate.sdpMLineIndex+1]+='a='+event.candidate.candidate+'\r\n';}else{sections[event.candidate.sdpMLineIndex+1]+='a=end-of-candidates\r\n';}
+self.localDescription.sdp=sections.join('');var transceivers=self._pendingOffer?self._pendingOffer:self.transceivers;var complete=transceivers.every(function(transceiver){return transceiver.iceGatherer&&transceiver.iceGatherer.state==='completed';});switch(self.iceGatheringState){case'new':if(!end){self._localIceCandidatesBuffer.push(event);}
+if(end&&complete){self._localIceCandidatesBuffer.push(new Event('icecandidate'));}
+break;case'gathering':self._emitBufferedCandidates();if(!end){self.dispatchEvent(event);if(self.onicecandidate!==null){self.onicecandidate(event);}}
+if(complete){self.dispatchEvent(new Event('icecandidate'));if(self.onicecandidate!==null){self.onicecandidate(new Event('icecandidate'));}
+self.iceGatheringState='complete';self._emitGatheringStateChange();}
+break;case'complete':break;default:break;}};return iceGatherer;};RTCPeerConnection.prototype._createIceAndDtlsTransports=function(){var self=this;var iceTransport=new window.RTCIceTransport(null);iceTransport.onicestatechange=function(){self._updateConnectionState();};var dtlsTransport=new window.RTCDtlsTransport(iceTransport);dtlsTransport.ondtlsstatechange=function(){self._updateConnectionState();};dtlsTransport.onerror=function(){Object.defineProperty(dtlsTransport,'state',{value:'failed',writable:true});self._updateConnectionState();};return{iceTransport:iceTransport,dtlsTransport:dtlsTransport};};RTCPeerConnection.prototype._disposeIceAndDtlsTransports=function(sdpMLineIndex){var iceGatherer=this.transceivers[sdpMLineIndex].iceGatherer;if(iceGatherer){delete iceGatherer.onlocalcandidate;delete this.transceivers[sdpMLineIndex].iceGatherer;}
+var iceTransport=this.transceivers[sdpMLineIndex].iceTransport;if(iceTransport){delete iceTransport.onicestatechange;delete this.transceivers[sdpMLineIndex].iceTransport;}
+var dtlsTransport=this.transceivers[sdpMLineIndex].dtlsTransport;if(dtlsTransport){delete dtlsTransport.ondtlssttatechange;delete dtlsTransport.onerror;delete this.transceivers[sdpMLineIndex].dtlsTransport;}};RTCPeerConnection.prototype._transceive=function(transceiver,send,recv){var params=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);if(send&&transceiver.rtpSender){params.encodings=transceiver.sendEncodingParameters;params.rtcp={cname:SDPUtils.localCName,compound:transceiver.rtcpParameters.compound};if(transceiver.recvEncodingParameters.length){params.rtcp.ssrc=transceiver.recvEncodingParameters[0].ssrc;}
+transceiver.rtpSender.send(params);}
+if(recv&&transceiver.rtpReceiver){if(transceiver.kind==='video'&&transceiver.recvEncodingParameters&&edgeVersion<15019){transceiver.recvEncodingParameters.forEach(function(p){delete p.rtx;});}
+params.encodings=transceiver.recvEncodingParameters;params.rtcp={cname:transceiver.rtcpParameters.cname,compound:transceiver.rtcpParameters.compound};if(transceiver.sendEncodingParameters.length){params.rtcp.ssrc=transceiver.sendEncodingParameters[0].ssrc;}
+transceiver.rtpReceiver.receive(params);}};RTCPeerConnection.prototype.setLocalDescription=function(description){var self=this;if(!isActionAllowedInSignalingState('setLocalDescription',description.type,this.signalingState)){var e=new Error('Can not set local '+description.type+' in state '+this.signalingState);e.name='InvalidStateError';if(arguments.length>2&&typeof arguments[2]==='function'){window.setTimeout(arguments[2],0,e);}
+return Promise.reject(e);}
+var sections;var sessionpart;if(description.type==='offer'){if(this._pendingOffer){sections=SDPUtils.splitSections(description.sdp);sessionpart=sections.shift();sections.forEach(function(mediaSection,sdpMLineIndex){var caps=SDPUtils.parseRtpParameters(mediaSection);self._pendingOffer[sdpMLineIndex].localCapabilities=caps;});this.transceivers=this._pendingOffer;delete this._pendingOffer;}}else if(description.type==='answer'){sections=SDPUtils.splitSections(self.remoteDescription.sdp);sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,'a=ice-lite').length>0;sections.forEach(function(mediaSection,sdpMLineIndex){var transceiver=self.transceivers[sdpMLineIndex];var iceGatherer=transceiver.iceGatherer;var iceTransport=transceiver.iceTransport;var dtlsTransport=transceiver.dtlsTransport;var localCapabilities=transceiver.localCapabilities;var remoteCapabilities=transceiver.remoteCapabilities;var rejected=SDPUtils.isRejected(mediaSection);if(!rejected&&!transceiver.isDatachannel){var remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);var remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);if(isIceLite){remoteDtlsParameters.role='server';}
+if(!self.usingBundle||sdpMLineIndex===0){iceTransport.start(iceGatherer,remoteIceParameters,isIceLite?'controlling':'controlled');dtlsTransport.start(remoteDtlsParameters);}
+var params=getCommonCapabilities(localCapabilities,remoteCapabilities);self._transceive(transceiver,params.codecs.length>0,false);}});}
+this.localDescription={type:description.type,sdp:description.sdp};switch(description.type){case'offer':this._updateSignalingState('have-local-offer');break;case'answer':this._updateSignalingState('stable');break;default:throw new TypeError('unsupported type "'+description.type+'"');}
+var hasCallback=arguments.length>1&&typeof arguments[1]==='function';if(hasCallback){var cb=arguments[1];window.setTimeout(function(){cb();if(self.iceGatheringState==='new'){self.iceGatheringState='gathering';self._emitGatheringStateChange();}
+self._emitBufferedCandidates();},0);}
+var p=Promise.resolve();p.then(function(){if(!hasCallback){if(self.iceGatheringState==='new'){self.iceGatheringState='gathering';self._emitGatheringStateChange();}
+window.setTimeout(self._emitBufferedCandidates.bind(self),500);}});return p;};RTCPeerConnection.prototype.setRemoteDescription=function(description){var self=this;if(!isActionAllowedInSignalingState('setRemoteDescription',description.type,this.signalingState)){var e=new Error('Can not set remote '+description.type+' in state '+this.signalingState);e.name='InvalidStateError';if(arguments.length>2&&typeof arguments[2]==='function'){window.setTimeout(arguments[2],0,e);}
+return Promise.reject(e);}
+var streams={};var receiverList=[];var sections=SDPUtils.splitSections(description.sdp);var sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,'a=ice-lite').length>0;var usingBundle=SDPUtils.matchPrefix(sessionpart,'a=group:BUNDLE ').length>0;this.usingBundle=usingBundle;var iceOptions=SDPUtils.matchPrefix(sessionpart,'a=ice-options:')[0];if(iceOptions){this.canTrickleIceCandidates=iceOptions.substr(14).split(' ').indexOf('trickle')>=0;}else{this.canTrickleIceCandidates=false;}
+sections.forEach(function(mediaSection,sdpMLineIndex){var lines=SDPUtils.splitLines(mediaSection);var kind=SDPUtils.getKind(mediaSection);var rejected=SDPUtils.isRejected(mediaSection);var protocol=lines[0].substr(2).split(' ')[2];var direction=SDPUtils.getDirection(mediaSection,sessionpart);var remoteMsid=SDPUtils.parseMsid(mediaSection);var mid=SDPUtils.getMid(mediaSection)||SDPUtils.generateIdentifier();if(kind==='application'&&protocol==='DTLS/SCTP'){self.transceivers[sdpMLineIndex]={mid:mid,isDatachannel:true};return;}
+var transceiver;var iceGatherer;var iceTransport;var dtlsTransport;var rtpReceiver;var sendEncodingParameters;var recvEncodingParameters;var localCapabilities;var track;var remoteCapabilities=SDPUtils.parseRtpParameters(mediaSection);var remoteIceParameters;var remoteDtlsParameters;if(!rejected){remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);remoteDtlsParameters.role='client';}
+recvEncodingParameters=SDPUtils.parseRtpEncodingParameters(mediaSection);var rtcpParameters=SDPUtils.parseRtcpParameters(mediaSection);var isComplete=SDPUtils.matchPrefix(mediaSection,'a=end-of-candidates',sessionpart).length>0;var cands=SDPUtils.matchPrefix(mediaSection,'a=candidate:').map(function(cand){return SDPUtils.parseCandidate(cand);}).filter(function(cand){return cand.component==='1'||cand.component===1;});if((description.type==='offer'||description.type==='answer')&&!rejected&&usingBundle&&sdpMLineIndex>0&&self.transceivers[sdpMLineIndex]){self._disposeIceAndDtlsTransports(sdpMLineIndex);self.transceivers[sdpMLineIndex].iceGatherer=self.transceivers[0].iceGatherer;self.transceivers[sdpMLineIndex].iceTransport=self.transceivers[0].iceTransport;self.transceivers[sdpMLineIndex].dtlsTransport=self.transceivers[0].dtlsTransport;if(self.transceivers[sdpMLineIndex].rtpSender){self.transceivers[sdpMLineIndex].rtpSender.setTransport(self.transceivers[0].dtlsTransport);}
+if(self.transceivers[sdpMLineIndex].rtpReceiver){self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(self.transceivers[0].dtlsTransport);}}
+if(description.type==='offer'&&!rejected){transceiver=self.transceivers[sdpMLineIndex]||self._createTransceiver(kind);transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=usingBundle&&sdpMLineIndex>0?self.transceivers[0].iceGatherer:self._createIceGatherer(mid,sdpMLineIndex);}
+if(isComplete&&(!usingBundle||sdpMLineIndex===0)){transceiver.iceTransport.setRemoteCandidates(cands);}
+localCapabilities=window.RTCRtpReceiver.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=='rtx';});}
+sendEncodingParameters=[{ssrc:(2*sdpMLineIndex+2)*1001}];if(direction==='sendrecv'||direction==='sendonly'){rtpReceiver=new window.RTCRtpReceiver(transceiver.dtlsTransport,kind);track=rtpReceiver.track;if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream();Object.defineProperty(streams[remoteMsid.stream],'id',{get:function(){return remoteMsid.stream;}});}
+Object.defineProperty(track,'id',{get:function(){return remoteMsid.track;}});streams[remoteMsid.stream].addTrack(track);receiverList.push([track,rtpReceiver,streams[remoteMsid.stream]]);}else{if(!streams.default){streams.default=new window.MediaStream();}
+streams.default.addTrack(track);receiverList.push([track,rtpReceiver,streams.default]);}}
+transceiver.localCapabilities=localCapabilities;transceiver.remoteCapabilities=remoteCapabilities;transceiver.rtpReceiver=rtpReceiver;transceiver.rtcpParameters=rtcpParameters;transceiver.sendEncodingParameters=sendEncodingParameters;transceiver.recvEncodingParameters=recvEncodingParameters;self._transceive(self.transceivers[sdpMLineIndex],false,direction==='sendrecv'||direction==='sendonly');}else if(description.type==='answer'&&!rejected){transceiver=self.transceivers[sdpMLineIndex];iceGatherer=transceiver.iceGatherer;iceTransport=transceiver.iceTransport;dtlsTransport=transceiver.dtlsTransport;rtpReceiver=transceiver.rtpReceiver;sendEncodingParameters=transceiver.sendEncodingParameters;localCapabilities=transceiver.localCapabilities;self.transceivers[sdpMLineIndex].recvEncodingParameters=recvEncodingParameters;self.transceivers[sdpMLineIndex].remoteCapabilities=remoteCapabilities;self.transceivers[sdpMLineIndex].rtcpParameters=rtcpParameters;if((isIceLite||isComplete)&&cands.length){iceTransport.setRemoteCandidates(cands);}
+if(!usingBundle||sdpMLineIndex===0){iceTransport.start(iceGatherer,remoteIceParameters,'controlling');dtlsTransport.start(remoteDtlsParameters);}
+self._transceive(transceiver,direction==='sendrecv'||direction==='recvonly',direction==='sendrecv'||direction==='sendonly');if(rtpReceiver&&(direction==='sendrecv'||direction==='sendonly')){track=rtpReceiver.track;if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream();}
+streams[remoteMsid.stream].addTrack(track);receiverList.push([track,rtpReceiver,streams[remoteMsid.stream]]);}else{if(!streams.default){streams.default=new window.MediaStream();}
+streams.default.addTrack(track);receiverList.push([track,rtpReceiver,streams.default]);}}else{delete transceiver.rtpReceiver;}}});this.remoteDescription={type:description.type,sdp:description.sdp};switch(description.type){case'offer':this._updateSignalingState('have-remote-offer');break;case'answer':this._updateSignalingState('stable');break;default:throw new TypeError('unsupported type "'+description.type+'"');}
+Object.keys(streams).forEach(function(sid){var stream=streams[sid];if(stream.getTracks().length){self.remoteStreams.push(stream);var event=new Event('addstream');event.stream=stream;self.dispatchEvent(event);if(self.onaddstream!==null){window.setTimeout(function(){self.onaddstream(event);},0);}
+receiverList.forEach(function(item){var track=item[0];var receiver=item[1];if(stream.id!==item[2].id){return;}
+var trackEvent=new Event('track');trackEvent.track=track;trackEvent.receiver=receiver;trackEvent.streams=[stream];self.dispatchEvent(trackEvent);if(self.ontrack!==null){window.setTimeout(function(){self.ontrack(trackEvent);},0);}});}});window.setTimeout(function(){if(!(self&&self.transceivers)){return;}
+self.transceivers.forEach(function(transceiver){if(transceiver.iceTransport&&transceiver.iceTransport.state==='new'&&transceiver.iceTransport.getRemoteCandidates().length>0){console.warn('Timeout for addRemoteCandidate. Consider sending '+'an end-of-candidates notification');transceiver.iceTransport.addRemoteCandidate({});}});},4000);if(arguments.length>1&&typeof arguments[1]==='function'){window.setTimeout(arguments[1],0);}
+return Promise.resolve();};RTCPeerConnection.prototype.close=function(){this.transceivers.forEach(function(transceiver){if(transceiver.iceTransport){transceiver.iceTransport.stop();}
+if(transceiver.dtlsTransport){transceiver.dtlsTransport.stop();}
+if(transceiver.rtpSender){transceiver.rtpSender.stop();}
+if(transceiver.rtpReceiver){transceiver.rtpReceiver.stop();}});this._updateSignalingState('closed');};RTCPeerConnection.prototype._updateSignalingState=function(newState){this.signalingState=newState;var event=new Event('signalingstatechange');this.dispatchEvent(event);if(this.onsignalingstatechange!==null){this.onsignalingstatechange(event);}};RTCPeerConnection.prototype._maybeFireNegotiationNeeded=function(){var self=this;if(this.signalingState!=='stable'||this.needNegotiation===true){return;}
+this.needNegotiation=true;window.setTimeout(function(){if(self.needNegotiation===false){return;}
+self.needNegotiation=false;var event=new Event('negotiationneeded');self.dispatchEvent(event);if(self.onnegotiationneeded!==null){self.onnegotiationneeded(event);}},0);};RTCPeerConnection.prototype._updateConnectionState=function(){var self=this;var newState;var states={'new':0,closed:0,connecting:0,checking:0,connected:0,completed:0,disconnected:0,failed:0};this.transceivers.forEach(function(transceiver){states[transceiver.iceTransport.state]++;states[transceiver.dtlsTransport.state]++;});states.connected+=states.completed;newState='new';if(states.failed>0){newState='failed';}else if(states.connecting>0||states.checking>0){newState='connecting';}else if(states.disconnected>0){newState='disconnected';}else if(states.new>0){newState='new';}else if(states.connected>0||states.completed>0){newState='connected';}
+if(newState!==self.iceConnectionState){self.iceConnectionState=newState;var event=new Event('iceconnectionstatechange');this.dispatchEvent(event);if(this.oniceconnectionstatechange!==null){this.oniceconnectionstatechange(event);}}};RTCPeerConnection.prototype.createOffer=function(){var self=this;if(this._pendingOffer){throw new Error('createOffer called while there is a pending offer.');}
+var offerOptions;if(arguments.length===1&&typeof arguments[0]!=='function'){offerOptions=arguments[0];}else if(arguments.length===3){offerOptions=arguments[2];}
+var numAudioTracks=this.transceivers.filter(function(t){return t.kind==='audio';}).length;var numVideoTracks=this.transceivers.filter(function(t){return t.kind==='video';}).length;if(offerOptions){if(offerOptions.mandatory||offerOptions.optional){throw new TypeError('Legacy mandatory/optional constraints not supported.');}
+if(offerOptions.offerToReceiveAudio!==undefined){if(offerOptions.offerToReceiveAudio===true){numAudioTracks=1;}else if(offerOptions.offerToReceiveAudio===false){numAudioTracks=0;}else{numAudioTracks=offerOptions.offerToReceiveAudio;}}
+if(offerOptions.offerToReceiveVideo!==undefined){if(offerOptions.offerToReceiveVideo===true){numVideoTracks=1;}else if(offerOptions.offerToReceiveVideo===false){numVideoTracks=0;}else{numVideoTracks=offerOptions.offerToReceiveVideo;}}}
+this.transceivers.forEach(function(transceiver){if(transceiver.kind==='audio'){numAudioTracks--;if(numAudioTracks<0){transceiver.wantReceive=false;}}else if(transceiver.kind==='video'){numVideoTracks--;if(numVideoTracks<0){transceiver.wantReceive=false;}}});while(numAudioTracks>0||numVideoTracks>0){if(numAudioTracks>0){this._createTransceiver('audio');numAudioTracks--;}
+if(numVideoTracks>0){this._createTransceiver('video');numVideoTracks--;}}
+var transceivers=sortTracks(this.transceivers);var sdp=SDPUtils.writeSessionBoilerplate(this._sdpSessionId);transceivers.forEach(function(transceiver,sdpMLineIndex){var track=transceiver.track;var kind=transceiver.kind;var mid=SDPUtils.generateIdentifier();transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=self.usingBundle&&sdpMLineIndex>0?transceivers[0].iceGatherer:self._createIceGatherer(mid,sdpMLineIndex);}
+var localCapabilities=window.RTCRtpSender.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=='rtx';});}
+localCapabilities.codecs.forEach(function(codec){if(codec.name==='H264'&&codec.parameters['level-asymmetry-allowed']===undefined){codec.parameters['level-asymmetry-allowed']='1';}});var sendEncodingParameters=[{ssrc:(2*sdpMLineIndex+1)*1001}];if(track){if(edgeVersion>=15019&&kind==='video'){sendEncodingParameters[0].rtx={ssrc:(2*sdpMLineIndex+1)*1001+1};}}
+if(transceiver.wantReceive){transceiver.rtpReceiver=new window.RTCRtpReceiver(transceiver.dtlsTransport,kind);}
+transceiver.localCapabilities=localCapabilities;transceiver.sendEncodingParameters=sendEncodingParameters;});if(this._config.bundlePolicy!=='max-compat'){sdp+='a=group:BUNDLE '+transceivers.map(function(t){return t.mid;}).join(' ')+'\r\n';}
+sdp+='a=ice-options:trickle\r\n';transceivers.forEach(function(transceiver,sdpMLineIndex){sdp+=SDPUtils.writeMediaSection(transceiver,transceiver.localCapabilities,'offer',transceiver.stream);sdp+='a=rtcp-rsize\r\n';});this._pendingOffer=transceivers;var desc=new window.RTCSessionDescription({type:'offer',sdp:sdp});if(arguments.length&&typeof arguments[0]==='function'){window.setTimeout(arguments[0],0,desc);}
+return Promise.resolve(desc);};RTCPeerConnection.prototype.createAnswer=function(){var sdp=SDPUtils.writeSessionBoilerplate(this._sdpSessionId);if(this.usingBundle){sdp+='a=group:BUNDLE '+this.transceivers.map(function(t){return t.mid;}).join(' ')+'\r\n';}
+this.transceivers.forEach(function(transceiver,sdpMLineIndex){if(transceiver.isDatachannel){sdp+='m=application 0 DTLS/SCTP 5000\r\n'+'c=IN IP4 0.0.0.0\r\n'+'a=mid:'+transceiver.mid+'\r\n';return;}
+if(transceiver.stream){var localTrack;if(transceiver.kind==='audio'){localTrack=transceiver.stream.getAudioTracks()[0];}else if(transceiver.kind==='video'){localTrack=transceiver.stream.getVideoTracks()[0];}
+if(localTrack){if(edgeVersion>=15019&&transceiver.kind==='video'){transceiver.sendEncodingParameters[0].rtx={ssrc:(2*sdpMLineIndex+2)*1001+1};}}}
+var commonCapabilities=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);var hasRtx=commonCapabilities.codecs.filter(function(c){return c.name.toLowerCase()==='rtx';}).length;if(!hasRtx&&transceiver.sendEncodingParameters[0].rtx){delete transceiver.sendEncodingParameters[0].rtx;}
+sdp+=SDPUtils.writeMediaSection(transceiver,commonCapabilities,'answer',transceiver.stream);if(transceiver.rtcpParameters&&transceiver.rtcpParameters.reducedSize){sdp+='a=rtcp-rsize\r\n';}});var desc=new window.RTCSessionDescription({type:'answer',sdp:sdp});if(arguments.length&&typeof arguments[0]==='function'){window.setTimeout(arguments[0],0,desc);}
+return Promise.resolve(desc);};RTCPeerConnection.prototype.addIceCandidate=function(candidate){if(!candidate){for(var j=0;j<this.transceivers.length;j++){this.transceivers[j].iceTransport.addRemoteCandidate({});if(this.usingBundle){return Promise.resolve();}}}else{var mLineIndex=candidate.sdpMLineIndex;if(candidate.sdpMid){for(var i=0;i<this.transceivers.length;i++){if(this.transceivers[i].mid===candidate.sdpMid){mLineIndex=i;break;}}}
+var transceiver=this.transceivers[mLineIndex];if(transceiver){var cand=Object.keys(candidate.candidate).length>0?SDPUtils.parseCandidate(candidate.candidate):{};if(cand.protocol==='tcp'&&(cand.port===0||cand.port===9)){return Promise.resolve();}
+if(cand.component&&!(cand.component==='1'||cand.component===1)){return Promise.resolve();}
+transceiver.iceTransport.addRemoteCandidate(cand);var sections=SDPUtils.splitSections(this.remoteDescription.sdp);sections[mLineIndex+1]+=(cand.type?candidate.candidate.trim():'a=end-of-candidates')+'\r\n';this.remoteDescription.sdp=sections.join('');}}
+if(arguments.length>1&&typeof arguments[1]==='function'){window.setTimeout(arguments[1],0);}
+return Promise.resolve();};RTCPeerConnection.prototype.getStats=function(){var promises=[];this.transceivers.forEach(function(transceiver){['rtpSender','rtpReceiver','iceGatherer','iceTransport','dtlsTransport'].forEach(function(method){if(transceiver[method]){promises.push(transceiver[method].getStats());}});});var cb=arguments.length>1&&typeof arguments[1]==='function'&&arguments[1];var fixStatsType=function(stat){return{inboundrtp:'inbound-rtp',outboundrtp:'outbound-rtp',candidatepair:'candidate-pair',localcandidate:'local-candidate',remotecandidate:'remote-candidate'}[stat.type]||stat.type;};return new Promise(function(resolve){var results=new Map();Promise.all(promises).then(function(res){res.forEach(function(result){Object.keys(result).forEach(function(id){result[id].type=fixStatsType(result[id]);results.set(id,result[id]);});});if(cb){window.setTimeout(cb,0,results);}
+resolve(results);});});};return RTCPeerConnection;};},{"sdp":1}],9:[function(require,module,exports){'use strict';var utils=require('../utils');var firefoxShim={shimOnTrack:function(window){if(typeof window==='object'&&window.RTCPeerConnection&&!('ontrack'in
+window.RTCPeerConnection.prototype)){Object.defineProperty(window.RTCPeerConnection.prototype,'ontrack',{get:function(){return this._ontrack;},set:function(f){if(this._ontrack){this.removeEventListener('track',this._ontrack);this.removeEventListener('addstream',this._ontrackpoly);}
+this.addEventListener('track',this._ontrack=f);this.addEventListener('addstream',this._ontrackpoly=function(e){e.stream.getTracks().forEach(function(track){var event=new Event('track');event.track=track;event.receiver={track:track};event.streams=[e.stream];this.dispatchEvent(event);}.bind(this));}.bind(this));}});}},shimSourceObject:function(window){if(typeof window==='object'){if(window.HTMLMediaElement&&!('srcObject'in window.HTMLMediaElement.prototype)){Object.defineProperty(window.HTMLMediaElement.prototype,'srcObject',{get:function(){return this.mozSrcObject;},set:function(stream){this.mozSrcObject=stream;}});}}},shimPeerConnection:function(window){var browserDetails=utils.detectBrowser(window);if(typeof window!=='object'||!(window.RTCPeerConnection||window.mozRTCPeerConnection)){return;}
+if(!window.RTCPeerConnection){window.RTCPeerConnection=function(pcConfig,pcConstraints){if(browserDetails.version<38){if(pcConfig&&pcConfig.iceServers){var newIceServers=[];for(var i=0;i<pcConfig.iceServers.length;i++){var server=pcConfig.iceServers[i];if(server.hasOwnProperty('urls')){for(var j=0;j<server.urls.length;j++){var newServer={url:server.urls[j]};if(server.urls[j].indexOf('turn')===0){newServer.username=server.username;newServer.credential=server.credential;}
+newIceServers.push(newServer);}}else{newIceServers.push(pcConfig.iceServers[i]);}}
+pcConfig.iceServers=newIceServers;}}
+return new window.mozRTCPeerConnection(pcConfig,pcConstraints);};window.RTCPeerConnection.prototype=window.mozRTCPeerConnection.prototype;if(window.mozRTCPeerConnection.generateCertificate){Object.defineProperty(window.RTCPeerConnection,'generateCertificate',{get:function(){return window.mozRTCPeerConnection.generateCertificate;}});}
+window.RTCSessionDescription=window.mozRTCSessionDescription;window.RTCIceCandidate=window.mozRTCIceCandidate;}
+['setLocalDescription','setRemoteDescription','addIceCandidate'].forEach(function(method){var nativeMethod=window.RTCPeerConnection.prototype[method];window.RTCPeerConnection.prototype[method]=function(){arguments[0]=new((method==='addIceCandidate')?window.RTCIceCandidate:window.RTCSessionDescription)(arguments[0]);return nativeMethod.apply(this,arguments);};});var nativeAddIceCandidate=window.RTCPeerConnection.prototype.addIceCandidate;window.RTCPeerConnection.prototype.addIceCandidate=function(){if(!arguments[0]){if(arguments[1]){arguments[1].apply(null);}
+return Promise.resolve();}
+return nativeAddIceCandidate.apply(this,arguments);};var makeMapStats=function(stats){var map=new Map();Object.keys(stats).forEach(function(key){map.set(key,stats[key]);map[key]=stats[key];});return map;};var modernStatsTypes={inboundrtp:'inbound-rtp',outboundrtp:'outbound-rtp',candidatepair:'candidate-pair',localcandidate:'local-candidate',remotecandidate:'remote-candidate'};var nativeGetStats=window.RTCPeerConnection.prototype.getStats;window.RTCPeerConnection.prototype.getStats=function(selector,onSucc,onErr){return nativeGetStats.apply(this,[selector||null]).then(function(stats){if(browserDetails.version<48){stats=makeMapStats(stats);}
+if(browserDetails.version<53&&!onSucc){try{stats.forEach(function(stat){stat.type=modernStatsTypes[stat.type]||stat.type;});}catch(e){if(e.name!=='TypeError'){throw e;}
+stats.forEach(function(stat,i){stats.set(i,Object.assign({},stat,{type:modernStatsTypes[stat.type]||stat.type}));});}}
+return stats;}).then(onSucc,onErr);};}};module.exports={shimOnTrack:firefoxShim.shimOnTrack,shimSourceObject:firefoxShim.shimSourceObject,shimPeerConnection:firefoxShim.shimPeerConnection,shimGetUserMedia:require('./getusermedia')};},{"../utils":12,"./getusermedia":10}],10:[function(require,module,exports){'use strict';var utils=require('../utils');var logging=utils.log;module.exports=function(window){var browserDetails=utils.detectBrowser(window);var navigator=window&&window.navigator;var MediaStreamTrack=window&&window.MediaStreamTrack;var shimError_=function(e){return{name:{InternalError:'NotReadableError',NotSupportedError:'TypeError',PermissionDeniedError:'NotAllowedError',SecurityError:'NotAllowedError'}[e.name]||e.name,message:{'The operation is insecure.':'The request is not allowed by the '+'user agent or the platform in the current context.'}[e.message]||e.message,constraint:e.constraint,toString:function(){return this.name+(this.message&&': ')+this.message;}};};var getUserMedia_=function(constraints,onSuccess,onError){var constraintsToFF37_=function(c){if(typeof c!=='object'||c.require){return c;}
+var require=[];Object.keys(c).forEach(function(key){if(key==='require'||key==='advanced'||key==='mediaSource'){return;}
+var r=c[key]=(typeof c[key]==='object')?c[key]:{ideal:c[key]};if(r.min!==undefined||r.max!==undefined||r.exact!==undefined){require.push(key);}
+if(r.exact!==undefined){if(typeof r.exact==='number'){r.min=r.max=r.exact;}else{c[key]=r.exact;}
+delete r.exact;}
+if(r.ideal!==undefined){c.advanced=c.advanced||[];var oc={};if(typeof r.ideal==='number'){oc[key]={min:r.ideal,max:r.ideal};}else{oc[key]=r.ideal;}
+c.advanced.push(oc);delete r.ideal;if(!Object.keys(r).length){delete c[key];}}});if(require.length){c.require=require;}
+return c;};constraints=JSON.parse(JSON.stringify(constraints));if(browserDetails.version<38){logging('spec: '+JSON.stringify(constraints));if(constraints.audio){constraints.audio=constraintsToFF37_(constraints.audio);}
+if(constraints.video){constraints.video=constraintsToFF37_(constraints.video);}
+logging('ff37: '+JSON.stringify(constraints));}
+return navigator.mozGetUserMedia(constraints,onSuccess,function(e){onError(shimError_(e));});};var getUserMediaPromise_=function(constraints){return new Promise(function(resolve,reject){getUserMedia_(constraints,resolve,reject);});};if(!navigator.mediaDevices){navigator.mediaDevices={getUserMedia:getUserMediaPromise_,addEventListener:function(){},removeEventListener:function(){}};}
+navigator.mediaDevices.enumerateDevices=navigator.mediaDevices.enumerateDevices||function(){return new Promise(function(resolve){var infos=[{kind:'audioinput',deviceId:'default',label:'',groupId:''},{kind:'videoinput',deviceId:'default',label:'',groupId:''}];resolve(infos);});};if(browserDetails.version<41){var orgEnumerateDevices=navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);navigator.mediaDevices.enumerateDevices=function(){return orgEnumerateDevices().then(undefined,function(e){if(e.name==='NotFoundError'){return[];}
+throw e;});};}
+if(browserDetails.version<49){var origGetUserMedia=navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);navigator.mediaDevices.getUserMedia=function(c){return origGetUserMedia(c).then(function(stream){if(c.audio&&!stream.getAudioTracks().length||c.video&&!stream.getVideoTracks().length){stream.getTracks().forEach(function(track){track.stop();});throw new DOMException('The object can not be found here.','NotFoundError');}
+return stream;},function(e){return Promise.reject(shimError_(e));});};}
+if(!(browserDetails.version>55&&'autoGainControl'in navigator.mediaDevices.getSupportedConstraints())){var remap=function(obj,a,b){if(a in obj&&!(b in obj)){obj[b]=obj[a];delete obj[a];}};var nativeGetUserMedia=navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);navigator.mediaDevices.getUserMedia=function(c){if(typeof c==='object'&&typeof c.audio==='object'){c=JSON.parse(JSON.stringify(c));remap(c.audio,'autoGainControl','mozAutoGainControl');remap(c.audio,'noiseSuppression','mozNoiseSuppression');}
+return nativeGetUserMedia(c);};if(MediaStreamTrack&&MediaStreamTrack.prototype.getSettings){var nativeGetSettings=MediaStreamTrack.prototype.getSettings;MediaStreamTrack.prototype.getSettings=function(){var obj=nativeGetSettings.apply(this,arguments);remap(obj,'mozAutoGainControl','autoGainControl');remap(obj,'mozNoiseSuppression','noiseSuppression');return obj;};}
+if(MediaStreamTrack&&MediaStreamTrack.prototype.applyConstraints){var nativeApplyConstraints=MediaStreamTrack.prototype.applyConstraints;MediaStreamTrack.prototype.applyConstraints=function(c){if(this.kind==='audio'&&typeof c==='object'){c=JSON.parse(JSON.stringify(c));remap(c,'autoGainControl','mozAutoGainControl');remap(c,'noiseSuppression','mozNoiseSuppression');}
+return nativeApplyConstraints.apply(this,[c]);};}}
+navigator.getUserMedia=function(constraints,onSuccess,onError){if(browserDetails.version<44){return getUserMedia_(constraints,onSuccess,onError);}
+console.warn('navigator.getUserMedia has been replaced by '+'navigator.mediaDevices.getUserMedia');navigator.mediaDevices.getUserMedia(constraints).then(onSuccess,onError);};};},{"../utils":12}],11:[function(require,module,exports){'use strict';var utils=require('../utils');var safariShim={shimLocalStreamsAPI:function(window){if(typeof window!=='object'||!window.RTCPeerConnection){return;}
+if(!('getLocalStreams'in window.RTCPeerConnection.prototype)){window.RTCPeerConnection.prototype.getLocalStreams=function(){if(!this._localStreams){this._localStreams=[];}
+return this._localStreams;};}
+if(!('getStreamById'in window.RTCPeerConnection.prototype)){window.RTCPeerConnection.prototype.getStreamById=function(id){var result=null;if(this._localStreams){this._localStreams.forEach(function(stream){if(stream.id===id){result=stream;}});}
+if(this._remoteStreams){this._remoteStreams.forEach(function(stream){if(stream.id===id){result=stream;}});}
+return result;};}
+if(!('addStream'in window.RTCPeerConnection.prototype)){var _addTrack=window.RTCPeerConnection.prototype.addTrack;window.RTCPeerConnection.prototype.addStream=function(stream){if(!this._localStreams){this._localStreams=[];}
+if(this._localStreams.indexOf(stream)===-1){this._localStreams.push(stream);}
+var self=this;stream.getTracks().forEach(function(track){_addTrack.call(self,track,stream);});};window.RTCPeerConnection.prototype.addTrack=function(track,stream){if(stream){if(!this._localStreams){this._localStreams=[stream];}else if(this._localStreams.indexOf(stream)===-1){this._localStreams.push(stream);}}
+_addTrack.call(this,track,stream);};}
+if(!('removeStream'in window.RTCPeerConnection.prototype)){window.RTCPeerConnection.prototype.removeStream=function(stream){if(!this._localStreams){this._localStreams=[];}
+var index=this._localStreams.indexOf(stream);if(index===-1){return;}
+this._localStreams.splice(index,1);var self=this;var tracks=stream.getTracks();this.getSenders().forEach(function(sender){if(tracks.indexOf(sender.track)!==-1){self.removeTrack(sender);}});};}},shimRemoteStreamsAPI:function(window){if(typeof window!=='object'||!window.RTCPeerConnection){return;}
+if(!('getRemoteStreams'in window.RTCPeerConnection.prototype)){window.RTCPeerConnection.prototype.getRemoteStreams=function(){return this._remoteStreams?this._remoteStreams:[];};}
+if(!('onaddstream'in window.RTCPeerConnection.prototype)){Object.defineProperty(window.RTCPeerConnection.prototype,'onaddstream',{get:function(){return this._onaddstream;},set:function(f){if(this._onaddstream){this.removeEventListener('addstream',this._onaddstream);this.removeEventListener('track',this._onaddstreampoly);}
+this.addEventListener('addstream',this._onaddstream=f);this.addEventListener('track',this._onaddstreampoly=function(e){var stream=e.streams[0];if(!this._remoteStreams){this._remoteStreams=[];}
+if(this._remoteStreams.indexOf(stream)>=0){return;}
+this._remoteStreams.push(stream);var event=new Event('addstream');event.stream=e.streams[0];this.dispatchEvent(event);}.bind(this));}});}},shimCallbacksAPI:function(window){if(typeof window!=='object'||!window.RTCPeerConnection){return;}
+var prototype=window.RTCPeerConnection.prototype;var createOffer=prototype.createOffer;var createAnswer=prototype.createAnswer;var setLocalDescription=prototype.setLocalDescription;var setRemoteDescription=prototype.setRemoteDescription;var addIceCandidate=prototype.addIceCandidate;prototype.createOffer=function(successCallback,failureCallback){var options=(arguments.length>=2)?arguments[2]:arguments[0];var promise=createOffer.apply(this,[options]);if(!failureCallback){return promise;}
+promise.then(successCallback,failureCallback);return Promise.resolve();};prototype.createAnswer=function(successCallback,failureCallback){var options=(arguments.length>=2)?arguments[2]:arguments[0];var promise=createAnswer.apply(this,[options]);if(!failureCallback){return promise;}
+promise.then(successCallback,failureCallback);return Promise.resolve();};var withCallback=function(description,successCallback,failureCallback){var promise=setLocalDescription.apply(this,[description]);if(!failureCallback){return promise;}
+promise.then(successCallback,failureCallback);return Promise.resolve();};prototype.setLocalDescription=withCallback;withCallback=function(description,successCallback,failureCallback){var promise=setRemoteDescription.apply(this,[description]);if(!failureCallback){return promise;}
+promise.then(successCallback,failureCallback);return Promise.resolve();};prototype.setRemoteDescription=withCallback;withCallback=function(candidate,successCallback,failureCallback){var promise=addIceCandidate.apply(this,[candidate]);if(!failureCallback){return promise;}
+promise.then(successCallback,failureCallback);return Promise.resolve();};prototype.addIceCandidate=withCallback;},shimGetUserMedia:function(window){var navigator=window&&window.navigator;if(!navigator.getUserMedia){if(navigator.webkitGetUserMedia){navigator.getUserMedia=navigator.webkitGetUserMedia.bind(navigator);}else if(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia){navigator.getUserMedia=function(constraints,cb,errcb){navigator.mediaDevices.getUserMedia(constraints).then(cb,errcb);}.bind(navigator);}}},shimRTCIceServerUrls:function(window){var OrigPeerConnection=window.RTCPeerConnection;window.RTCPeerConnection=function(pcConfig,pcConstraints){if(pcConfig&&pcConfig.iceServers){var newIceServers=[];for(var i=0;i<pcConfig.iceServers.length;i++){var server=pcConfig.iceServers[i];if(!server.hasOwnProperty('urls')&&server.hasOwnProperty('url')){utils.deprecated('RTCIceServer.url','RTCIceServer.urls');server=JSON.parse(JSON.stringify(server));server.urls=server.url;delete server.url;newIceServers.push(server);}else{newIceServers.push(pcConfig.iceServers[i]);}}
+pcConfig.iceServers=newIceServers;}
+return new OrigPeerConnection(pcConfig,pcConstraints);};window.RTCPeerConnection.prototype=OrigPeerConnection.prototype;Object.defineProperty(window.RTCPeerConnection,'generateCertificate',{get:function(){return OrigPeerConnection.generateCertificate;}});}};module.exports={shimCallbacksAPI:safariShim.shimCallbacksAPI,shimLocalStreamsAPI:safariShim.shimLocalStreamsAPI,shimRemoteStreamsAPI:safariShim.shimRemoteStreamsAPI,shimGetUserMedia:safariShim.shimGetUserMedia,shimRTCIceServerUrls:safariShim.shimRTCIceServerUrls};},{"../utils":12}],12:[function(require,module,exports){'use strict';var logDisabled_=true;var deprecationWarnings_=true;var utils={disableLog:function(bool){if(typeof bool!=='boolean'){return new Error('Argument type: '+typeof bool+'. Please use a boolean.');}
+logDisabled_=bool;return(bool)?'adapter.js logging disabled':'adapter.js logging enabled';},disableWarnings:function(bool){if(typeof bool!=='boolean'){return new Error('Argument type: '+typeof bool+'. Please use a boolean.');}
+deprecationWarnings_=!bool;return'adapter.js deprecation warnings '+(bool?'disabled':'enabled');},log:function(){if(typeof window==='object'){if(logDisabled_){return;}
+if(typeof console!=='undefined'&&typeof console.log==='function'){console.log.apply(console,arguments);}}},deprecated:function(oldMethod,newMethod){if(!deprecationWarnings_){return;}
+console.warn(oldMethod+' is deprecated, please use '+newMethod+' instead.');},extractVersion:function(uastring,expr,pos){var match=uastring.match(expr);return match&&match.length>=pos&&parseInt(match[pos],10);},detectBrowser:function(window){var navigator=window&&window.navigator;var result={};result.browser=null;result.version=null;if(typeof window==='undefined'||!window.navigator){result.browser='Not a browser.';return result;}
+if(navigator.mozGetUserMedia){result.browser='firefox';result.version=this.extractVersion(navigator.userAgent,/Firefox\/(\d+)\./,1);}else if(navigator.webkitGetUserMedia){if(window.webkitRTCPeerConnection){result.browser='chrome';result.version=this.extractVersion(navigator.userAgent,/Chrom(e|ium)\/(\d+)\./,2);}else{if(navigator.userAgent.match(/Version\/(\d+).(\d+)/)){result.browser='safari';result.version=this.extractVersion(navigator.userAgent,/AppleWebKit\/(\d+)\./,1);}else{result.browser='Unsupported webkit-based browser '+'with GUM support but no WebRTC support.';return result;}}}else if(navigator.mediaDevices&&navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)){result.browser='edge';result.version=this.extractVersion(navigator.userAgent,/Edge\/(\d+).(\d+)$/,2);}else if(navigator.mediaDevices&&navigator.userAgent.match(/AppleWebKit\/(\d+)\./)){result.browser='safari';result.version=this.extractVersion(navigator.userAgent,/AppleWebKit\/(\d+)\./,1);}else{result.browser='Not a supported browser.';return result;}
+return result;},shimCreateObjectURL:function(window){var URL=window&&window.URL;if(!(typeof window==='object'&&window.HTMLMediaElement&&'srcObject'in window.HTMLMediaElement.prototype)){return undefined;}
+var nativeCreateObjectURL=URL.createObjectURL.bind(URL);var nativeRevokeObjectURL=URL.revokeObjectURL.bind(URL);var streams=new Map(),newId=0;URL.createObjectURL=function(stream){if('getTracks'in stream){var url='polyblob:'+(++newId);streams.set(url,stream);utils.deprecated('URL.createObjectURL(stream)','elem.srcObject = stream');return url;}
+return nativeCreateObjectURL(stream);};URL.revokeObjectURL=function(url){nativeRevokeObjectURL(url);streams.delete(url);};var dsc=Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,'src');Object.defineProperty(window.HTMLMediaElement.prototype,'src',{get:function(){return dsc.get.apply(this);},set:function(url){this.srcObject=streams.get(url)||null;return dsc.set.apply(this,[url]);}});var nativeSetAttribute=window.HTMLMediaElement.prototype.setAttribute;window.HTMLMediaElement.prototype.setAttribute=function(){if(arguments.length===2&&(''+arguments[0]).toLowerCase()==='src'){this.srcObject=streams.get(arguments[1])||null;}
+return nativeSetAttribute.apply(this,arguments);};}};module.exports={log:utils.log,deprecated:utils.deprecated,disableLog:utils.disableLog,disableWarnings:utils.disableWarnings,extractVersion:utils.extractVersion,shimCreateObjectURL:utils.shimCreateObjectURL,detectBrowser:utils.detectBrowser.bind(utils)};},{}]},{},[2])(2)});
diff --git a/bigbluebutton-html5/client/compatibility/verto_extension.js b/bigbluebutton-html5/client/compatibility/verto_extension.js
new file mode 100644
index 0000000000000000000000000000000000000000..65a67535faa96e9b2247263cd9cbbb89615ae30d
--- /dev/null
+++ b/bigbluebutton-html5/client/compatibility/verto_extension.js
@@ -0,0 +1,706 @@
+Verto = function (
+  tag,
+  voiceBridge,
+  conferenceUsername,
+  userCallback,
+  onFail = null,
+  chromeExtension = null,
+  stunTurnInfo = null,
+  loadingCallback = null) {
+
+  voiceBridge += "-SCREENSHARE";
+  this.cur_call = null;
+  this.share_call = null;
+  this.vertoHandle;
+
+  this.vid_width = Math.max(window.screen.width, 1920);
+  this.vid_height = Math.max(window.screen.height, 1080);
+
+  this.outgoingBandwidth = "default";
+  this.incomingBandwidth = "default";
+  // this.sessid = $.verto.genUUID();
+  this.sessid = Math.random().toString();
+
+  this.renderTag = 'remote-media';
+
+  this.destination_number = voiceBridge;
+  this.caller_id_name = conferenceUsername;
+  this.caller_id_number = conferenceUsername;
+
+  this.vertoPort = "verto";
+  this.hostName = window.location.hostname;
+  this.socketUrl = 'wss://' + this.hostName + '/' + this.vertoPort;
+  this.login = "bbbuser";
+  this.password = "secret";
+
+  this.useVideo = false;
+  this.useCamera = false;
+  this.useMic = false;
+
+  this.callWasSuccessful = false;
+  this.shouldConnect = true;
+  this.iceServers = stunTurnInfo;
+  this.userCallback = userCallback;
+
+  if (loadingCallback != null) {
+    this.videoLoadingCallback = Verto.normalizeCallback(loadingCallback);
+  } else {
+    this.videoLoadingCallback = function() {};
+  }
+
+  if (chromeExtension != null) {
+    this.chromeExtension = chromeExtension;
+  }
+
+  if (onFail != null) {
+    this.onFail = Verto.normalizeCallback(onFail);
+  } else {
+    var _this = this;
+    this.onFail = function () {
+      _this.logError('Default error handler');
+    };
+  }
+};
+
+Verto.prototype.logger = function (obj) {
+  console.log(obj);
+};
+
+Verto.prototype.logError = function (obj) {
+  console.error(obj);
+};
+
+Verto.prototype.setRenderTag = function (tag) {
+  this.renderTag = tag;
+};
+
+// receives either a string variable holding the name of an actionscript
+// registered callback, or a javascript function object.
+// The function will return either the function if it is a javascript Function
+// or if it is an actionscript string it will return a javascript Function
+// that when invokved will invoke the actionscript registered callback
+// and passes along parameters
+Verto.normalizeCallback = function (callback) {
+  if (typeof callback == 'function') {
+    return callback;
+  } else {
+    return function (args) {
+      document.getElementById('BigBlueButton')[callback](args);
+    };
+  }
+};
+
+Verto.prototype.onWSLogin = function (v, success) {
+  this.cur_call = null;
+  if (success) {
+    if (!this.shouldConnect) {
+      return;
+    }
+
+    this.callWasSuccessful = true;
+    this.mediaCallback();
+    return;
+  } else {
+    // error logging verto into freeswitch
+    this.logError({ status: 'failed', errorcode: '10XX' });
+    this.callWasSuccessful = false;
+    this.onFail();
+    return;
+  }
+};
+
+Verto.prototype.registerCallbacks = function () {
+  var callbacks = {
+    onMessage: function (verto, dialog, msg, data) {
+
+      switch (msg) {
+        case $.verto.enum.message.pvtEvent:
+          if (data.pvtData) {
+            switch (data.pvtData.action) {
+              // This client has joined the live array for the conference.
+              case "conference-liveArray-join":
+                initLiveArray(verto, dialog, data);
+                break;
+              // This client has left the live array for the conference.
+              case "conference-liveArray-part":
+                // Some kind of client-side wrapup...
+              break;
+            }
+          }
+          break;
+      }
+
+    },
+
+    onDialogState: function (d) {},
+
+    onWSLogin: this.onWSLogin.bind(this),
+
+    onWSClose: function (v, success) {
+      cur_call = null;
+      if (this.callWasSuccessful) {
+        // the connection was dropped in an already established call
+        this.logError('websocket disconnected');
+
+        // WebSocket disconnected
+        this.logError({ status:  'failed', errorcode: 1001 });
+        toDisplayDisconnectCallback = false;
+      } else {
+        // this callback was triggered and a call was never successfully established
+        this.logError('websocket connection could not be established');
+
+        // Could not make a WebSocket connection
+        this.logError({ status:  'failed', errorcode: 1002 });
+        this.onFail();
+        return;
+      }
+    }.bind(this),
+  };
+  this.callbacks = callbacks;
+};
+
+var initLiveArray = function(verto, dialog, data) {
+    // Set up addtional configuration specific to the call.
+    window.vertoConf = new $.verto.conf(verto, {
+      dialog: dialog,
+      hasVid: true,
+      laData: data.pvtData,
+      // For subscribing to published chat messages.
+      chatCallback: function(verto, eventObj) {
+        var from = eventObj.data.fromDisplay || eventObj.data.from || 'Unknown';
+        var message = eventObj.data.message || '';
+      },
+    });
+    var config = {
+      subParams: {
+        callID: dialog ? dialog.callID : null
+      },
+    };
+    // Set up the live array, using the live array data received from FreeSWITCH.
+    window.liveArray = new $.verto.liveArray(window.vertoHandle, data.pvtData.laChannel, data.pvtData.laName, config);
+    // Subscribe to live array changes.
+    window.liveArray.onChange = function(liveArrayObj, args) {
+      console.log("Call UUID is: " + args.key);
+      console.log("Call data is: ", args.data);
+
+      console.log(liveArrayObj);
+      console.log(args);
+
+      try {
+        switch (args.action) {
+
+          // Initial list of existing conference users.
+          case "bootObj":
+            break;
+
+          // New user joined conference.
+          case "add":
+            break;
+
+          // User left conference.
+          case "del":
+            break;
+
+          // Existing user's state changed (mute/unmute, talking, floor, etc)
+          case "modify":
+            break;
+        }
+      } catch (err) {
+        console.error("ERROR: " + err);
+      }
+    };
+    // Called if the live array throws an error.
+    window.liveArray.onErr = function (obj, args) {
+      console.error("Error: ", obj, args);
+    };
+};
+
+Verto.prototype.hold = function () {
+  this.cur_call.toggleHold();
+};
+
+Verto.prototype.hangup = function () {
+  if (this.cur_call) {
+    // the duration of the call
+    if (this.cur_call.audioStream) {
+      this.logger('call ended ' + this.cur_call.audioStream.currentTime);
+    }
+
+    this.cur_call.hangup();
+    this.cur_call = null;
+  }
+
+  if (this.share_call) {
+    if (this.share_call.state == $.verto.enum.state.active) {
+      this.shouldConnect = false;
+    } else {
+      this.shouldConnect = true;
+    }
+
+    // the duration of the call
+    this.logger('call ended ' + this.share_call.audioStream.currentTime);
+    this.share_call.rtc.localStream.getTracks().forEach(track => track.stop());
+    this.share_call.hangup();
+    this.share_call = null;
+  }
+
+  // the user ended the call themself
+  // if (callPurposefullyEnded === true) {
+  if (true) {
+    this.logger({ status: 'ended' });
+  } else {
+    // Call ended unexpectedly
+    this.logError({ status: 'failed', errorcode: 1005 });
+  }
+};
+
+Verto.prototype.mute = function () {
+  this.cur_call.dtmf('0');
+};
+
+Verto.prototype.localmute = function () {
+  // var muted = cur_call.setMute('toggle');
+  // if (muted) {
+  //   display('Talking to: ' + cur_call.cidString() + ' [LOCALLY MUTED]');
+  // } else {
+  //   display('Talking to: ' + cur_call.cidString());
+  // }
+};
+
+Verto.prototype.localvidmute = function () {
+  // var muted = cur_call.setVideoMute('toggle');
+  // if (muted) {
+  //   display('Talking to: ' + cur_call.cidString() + ' [VIDEO LOCALLY MUTED]');
+  // } else {
+  //   display('Talking to: ' + cur_call.cidString());
+  // }
+};
+
+Verto.prototype.vmute = function () {
+  this.cur_call.dtmf('*0');
+};
+
+Verto.prototype.setWatchVideo = function (tag) {
+  this.mediaCallback = this.docall;
+  this.useVideo = true;
+  this.useCamera = 'none';
+  this.useMic = 'none';
+  this.create(tag);
+};
+
+Verto.prototype.setListenOnly = function (tag) {
+  this.mediaCallback = this.docall;
+  this.useVideo = false;
+  this.useCamera = 'none';
+  this.useMic = 'none';
+  this.create(tag);
+};
+
+Verto.prototype.setMicrophone = function (tag) {
+  this.mediaCallback = this.docall;
+  this.useVideo = false;
+  this.useCamera = 'none';
+  this.useMic = 'any';
+  this.create(tag);
+};
+
+Verto.prototype.setScreenShare = function (tag) {
+  // required for Verto to know we want to use video
+  // tell Verto we want to share webcam so it knows there will be a video stream
+  // but instead of a webcam we pass screen constraints
+  this.useCamera = 'any';
+  this.useMic = 'none';
+  this.mediaCallback = this.makeShare;
+  this.create(tag);
+};
+
+Verto.prototype.create = function (tag) {
+  this.setRenderTag(tag);
+  this.registerCallbacks();
+
+  // fetch ice information from server
+  if (this.iceServers == null) {
+    this.configStuns(this.init);
+  } else {
+    // already have it. proceed with init
+    this.init();
+  }
+};
+
+Verto.prototype.docall = function () {
+  if (this.cur_call) {
+    this.logger('Quitting: Call already in progress');
+    return;
+  }
+
+  this.shouldConnect = true;
+
+  this.cur_call = window.vertoHandle.newCall({
+    destination_number: this.destination_number,
+    caller_id_name: this.caller_id_name,
+    caller_id_number: this.caller_id_number,
+    outgoingBandwidth: this.outgoingBandwidth,
+    incomingBandwidth: this.incomingBandwidth,
+    useVideo: this.useVideo,
+    useStereo: true,
+    useCamera: this.useCamera,
+    useMic: this.useMic,
+    useSpeak: 'any',
+    dedEnc: true,
+    tag: this.renderTag,
+  });
+  this.logger(this.cur_call);
+};
+
+Verto.prototype.makeShare = function () {
+  if (this.share_call) {
+    this.logError('Quitting: Call already in progress');
+    return;
+  }
+
+  var screenInfo = null;
+  if (!!navigator.mozGetUserMedia) {
+    // no screen parameters for FF, just screenShare: true down below
+    screenInfo = {};
+    this.doShare(screenInfo);
+  } else if (!!window.chrome) {
+    var _this = this;
+    if (!_this.chromeExtension) {
+      _this.logError({
+        status:  'failed',
+        message: 'Missing Chrome Extension key',
+      });
+      _this.onFail();
+      return;
+    }
+
+    // bring up Chrome screen picker
+    getMyScreenConstraints(function (constraints) {
+      if (constraints == null || constraints == "" || constraints.streamId == null || constraints.streamId == "") {
+        _this.onFail();
+        return _this.logError(constraints);
+      }
+
+      screenInfo = {
+        chromeMediaSource: "desktop",
+        chromeMediaSourceId: constraints.streamId,
+      };
+
+      _this.logger(screenInfo);
+      _this.doShare(screenInfo);
+    }, _this.chromeExtension);
+  }
+};
+
+Verto.prototype.doShare = function (screenConstraints) {
+  this.shouldConnect = true;
+  screenConstraints.maxWidth = this.vid_width;
+  screenConstraints.maxHeight = this.vid_height;
+
+  this.share_call = window.vertoHandle.newCall({
+    destination_number: this.destination_number,
+    caller_id_name: this.caller_id_name,
+    caller_id_number: this.caller_id_number,
+    outgoingBandwidth: "default",
+    incomingBandwidth: "default",
+    videoParams: screenConstraints,
+    useVideo: true,
+    screenShare: true,
+    dedEnc: true,
+    mirrorInput: false,
+    tag: this.renderTag,
+  });
+
+  var stopSharing = function() {
+    console.log("stopSharing");
+    this.share_call.hangup();
+    this.share_call = null;
+  };
+
+  var _this = this;
+  // Override onStream callback in $.FSRTC instance
+  this.share_call.rtc.options.callbacks.onStream = function (rtc, stream) {
+    _this.videoLoadingCallback();
+
+    if (stream) {
+      var StreamTrack = stream.getVideoTracks()[0];
+      StreamTrack.addEventListener('ended', stopSharing.bind(_this));
+    }
+  };
+};
+
+Verto.prototype.init = function () {
+  this.cur_call = null;
+
+  if (!window.vertoHandle) {
+    window.vertoHandle = new $.verto({
+      useVideo: true,
+      login: this.login,
+      passwd: this.password,
+      socketUrl: this.socketUrl,
+      tag: this.renderTag,
+      ringFile: 'sounds/bell_ring2.wav',
+      sessid: this.sessid,
+      videoParams: {
+        minFrameRate: 15,
+        vertoBestFrameRate: 30,
+      },
+
+      deviceParams: {
+        useCamera: false,
+        useMic: false,
+        useSpeak: 'none',
+      },
+
+      audioParams: {
+        googAutoGainControl: false,
+        googNoiseSuppression: false,
+        googHighpassFilter: false,
+      },
+
+      iceServers: this.iceServers,
+    }, this.callbacks);
+  } else {
+    this.mediaCallback();
+    return;
+  }
+};
+
+Verto.prototype.configStuns = function (callback) {
+  this.logger('Fetching STUN/TURN server info for Verto initialization');
+  var _this = this;
+  var stunsConfig = {};
+
+  // flash client has api access. html5 user passes array.
+  // client provided no stuns and cannot make api calls
+  // use defaults in verto and try making a call
+  if (BBB.getSessionToken == undefined) {
+    // uses defaults
+    this.iceServers = true;
+    // run init callback
+    return callback();
+  }
+
+  // TODO: screensharing and audio use this exact same function. Should be
+  // moved to a shared library for retrieving stun/turn and just pass 
+  // success/fail callbacks
+  BBB.getSessionToken(function(sessionToken) {
+    $.ajax({
+      dataType: 'json',
+      url: '/bigbluebutton/api/stuns/',
+      data: {sessionToken},
+    }).done(function (data) {
+      _this.logger('ajax request done');
+      _this.logger(data);
+      if (data.response && data.response.returncode == 'FAILED') {
+        _this.logError(data.response.message, { error: true });
+        _this.logError({ status: 'failed', errorcode: data.response.message });
+        return;
+      }
+
+      stunsConfig.stunServers = (data.stunServers ? data.stunServers.map(function (data) {
+        return { url: data.url };
+      }) : []);
+
+      stunsConfig.turnServers = (data.turnServers ? data.turnServers.map(function (data) {
+        return {
+          urls: data.url,
+          username: data.username,
+          credential: data.password,
+        };
+      }) : []);
+
+      stunsConfig = stunsConfig.stunServers.concat(stunsConfig.turnServers);
+      _this.logger('success got stun data, making verto');
+      _this.iceServers = stunsConfig;
+      callback.apply(_this);
+    }).fail(function (data, textStatus, errorThrown) {
+      _this.logError({ status: 'failed', errorcode: 1009 });
+      _this.onFail();
+      return;
+    });
+  });
+};
+
+// checks whether Google Chrome or Firefox have the WebRTCPeerConnection object
+Verto.prototype.isWebRTCAvailable = function () {
+  return (typeof window.webkitRTCPeerConnection !== 'undefined' ||
+    typeof window.mozRTCPeerConnection !== 'undefined');
+};
+
+this.VertoManager = function () {
+  this.vertoAudio = null;
+  this.vertoVideo = null;
+  this.vertoScreenShare = null;
+  window.vertoHandle = null;
+};
+
+Verto.prototype.logout = function () {
+  this.exitAudio();
+  this.exitVideo();
+  this.exitScreenShare();
+  window.vertoHandle.logout();
+};
+
+VertoManager.prototype.exitAudio = function () {
+  if (this.vertoAudio != null) {
+    console.log('Hanging up vertoAudio');
+    this.vertoAudio.hangup();
+  }
+};
+
+VertoManager.prototype.exitVideo = function () {
+  if (this.vertoVideo != null) {
+    console.log('Hanging up vertoVideo');
+    this.vertoVideo.hangup();
+  }
+};
+
+VertoManager.prototype.exitScreenShare = function () {
+  if (this.vertoScreenShare != null) {
+    console.log('Hanging up vertoScreenShare');
+    this.vertoScreenShare.hangup();
+  }
+};
+
+VertoManager.prototype.joinListenOnly = function (tag) {
+  this.exitAudio();
+
+  if (this.vertoAudio == null) {
+    var obj = Object.create(Verto.prototype);
+    Verto.apply(obj, arguments);
+    this.vertoAudio = obj;
+  }
+
+  this.vertoAudio.setListenOnly(tag);
+};
+
+VertoManager.prototype.joinMicrophone = function (tag) {
+  this.exitAudio();
+
+  if (this.vertoAudio == null) {
+    var obj = Object.create(Verto.prototype);
+    Verto.apply(obj, arguments);
+    this.vertoAudio = obj;
+  }
+
+  this.vertoAudio.setMicrophone(tag);
+};
+
+VertoManager.prototype.joinWatchVideo = function (tag) {
+  this.exitVideo();
+
+  if (this.vertoVideo == null) {
+    var obj = Object.create(Verto.prototype);
+    Verto.apply(obj, arguments);
+    this.vertoVideo = obj;
+  }
+
+  this.vertoVideo.setWatchVideo(tag);
+};
+
+VertoManager.prototype.shareScreen = function (tag) {
+  this.exitScreenShare();
+
+  if (this.vertoScreenShare == null) {
+    var obj = Object.create(Verto.prototype);
+    Verto.apply(obj, arguments);
+    this.vertoScreenShare = obj;
+  }
+
+  this.vertoScreenShare.setScreenShare(tag);
+};
+
+window.vertoInitialize = function () {
+  if (window.vertoManager == null || window.vertoManager == undefined) {
+    window.vertoManager = new VertoManager();
+  }
+};
+
+window.vertoExitAudio = function () {
+  window.vertoInitialize();
+  window.vertoManager.exitAudio();
+};
+
+window.vertoExitVideo = function () {
+  window.vertoInitialize();
+  window.vertoManager.exitVideo();
+};
+
+window.vertoExitScreenShare = function () {
+  window.vertoInitialize();
+  window.vertoManager.exitScreenShare();
+};
+
+window.vertoJoinListenOnly = function () {
+  window.vertoInitialize();
+  window.vertoManager.joinListenOnly.apply(window.vertoManager, arguments);
+};
+
+window.vertoJoinMicrophone = function () {
+  window.vertoInitialize();
+  window.vertoManager.joinMicrophone.apply(window.vertoManager, arguments);
+};
+
+window.vertoWatchVideo = function () {
+  window.vertoInitialize();
+  window.vertoManager.joinWatchVideo.apply(window.vertoManager, arguments);
+};
+
+window.vertoShareScreen = function () {
+  window.vertoInitialize();
+  window.vertoManager.shareScreen.apply(window.vertoManager, arguments);
+};
+
+// a function to check whether the browser (Chrome only) is in an isIncognito
+// session. Requires 1 mandatory callback that only gets called if the browser
+// session is incognito. The callback for not being incognito is optional.
+// Attempts to retrieve the chrome filesystem API.
+window.checkIfIncognito = function(isIncognito, isNotIncognito = function () {}) {
+  isIncognito = Verto.normalizeCallback(isIncognito);
+  isNotIncognito = Verto.normalizeCallback(isNotIncognito);
+
+  var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
+  if (!fs) {
+    isNotIncognito();
+    return;
+  }
+  fs(window.TEMPORARY, 100, function(){isNotIncognito()}, function(){isIncognito()});
+};
+
+window.checkChromeExtInstalled = function (callback, chromeExtensionId) {
+  callback = Verto.normalizeCallback(callback);
+
+  if (typeof chrome === "undefined" || !chrome || !chrome.runtime) {
+    // No API, so no extension for sure
+    callback(false);
+    return;
+  }
+  chrome.runtime.sendMessage(
+    chromeExtensionId,
+    { getVersion: true },
+    function (response) {
+      if (!response || !response.version) {
+        // Communication failure - assume that no endpoint exists
+        callback(false);
+        return;
+      }
+      callback(true);
+    }
+  );
+}
+
+window.getMyScreenConstraints = function(theCallback, extensionId) {
+  theCallback = Verto.normalizeCallback(theCallback);
+  chrome.runtime.sendMessage(extensionId, {
+    getStream: true,
+    sources: [
+      "window",
+      "screen",
+      "tab"
+    ]},
+    function(response) {
+      console.log(response);
+      theCallback(response);
+   });
+};
diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
index e29b01b158425285740c30daf634096ab0ae608f..f4ced0416489d75b5a1bee589e2bd52c0553e678 100644
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -46,16 +46,7 @@
 </head>
 <body style="background-color: #06172A">
   <div id="app" role="document"></div>
-  <script src="/client/lib/bowser.js"></script>
-  <script src="/client/lib/sip.js"></script>
-  <script src="/client/lib/bbb_webrtc_bridge_sip.js"></script>
-  <script src="/client/lib/bbblogger.js"></script>
-  <script src="/client/lib/jquery.json-2.4.min.js"></script>
-  <script src="/client/lib/verto-min.js"></script>
-  <script src="/client/lib/verto_extension.js"></script>
-  <script src="/client/lib/reconnecting-websocket.min.js"></script>
-  <script src="/client/lib/kurento-utils.js"></script>
-  <script src="/client/lib/kurento-extension.js"></script>
-  <script src="/html5client/js/adapter.js"></script>
-  <script src="/html5client/js/adjust-videos.js"></script>
+  <audio id="remote-media" autoPlay="autoplay">
+    <track kind="captions" /> {/* These captions are brought to you by eslint */}
+  </audio>
 </body>
diff --git a/bigbluebutton-html5/client/main.jsx b/bigbluebutton-html5/client/main.jsx
old mode 100755
new mode 100644
diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js
index 4aa61b73a820007222945324e240caf0e0303291..0e20f98f4b15ee5997b73ed85207438f62f5e1d8 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods.js
@@ -5,15 +5,15 @@ import validateAuthToken from './methods/validateAuthToken';
 import setEmojiStatus from './methods/setEmojiStatus';
 import assignPresenter from './methods/assignPresenter';
 import changeRole from './methods/changeRole';
-import kickUser from './methods/kickUser';
+import removeUser from './methods/removeUser';
 
 Meteor.methods(mapToAcl(['methods.userLogout', 'methods.setEmojiStatus', 'methods.assignPresenter', 'methods.changeRole',
-  'methods.kickUser'], {
+  'methods.removeUser'], {
     userLogout,
     setEmojiStatus,
     assignPresenter,
     changeRole,
-    kickUser,
+    removeUser,
   }));
 
 Meteor.methods({ validateAuthToken, });
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js
similarity index 91%
rename from bigbluebutton-html5/imports/api/users/server/methods/kickUser.js
rename to bigbluebutton-html5/imports/api/users/server/methods/removeUser.js
index 645f68f012b5a3b109649aa08a5fd4756aaf2c25..80d90ec221d9a27e1e9ba862d8c07cf592ea9a47 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js
@@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
 import { check } from 'meteor/check';
 import RedisPubSub from '/imports/startup/server/redis';
 
-export default function kickUser(credentials, userId) {
+export default function removeUser(credentials, userId) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
   const EVENT_NAME = 'EjectUserFromMeetingCmdMsg';
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js b/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
index 5fb2e788b3a950300cc9b731ad6bae99e35389f7..2bfee14015ac597f5e24c5746aecfe27c0e70ddd 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
@@ -13,8 +13,14 @@ export default function handleVoiceUpdate({ body }, meetingId) {
     voiceUserId: String,
   });
 
-  const { intId } = voiceUser;
+  const { intId, voiceUserId } = voiceUser;
+
+  const isDialInUser = (voiceUserId) => {
+    return voiceUserId && (voiceUserId[0] == 'v');
+  }
+
+  // if the user is dial-in, leaving voice also means leaving userlist
+  if(isDialInUser(voiceUserId)) removeUser(meetingId, intId);
 
-  removeUser(meetingId, intId);
   return removeVoiceUser(meetingId, voiceUser);
 }
diff --git a/bigbluebutton-html5/imports/startup/client/auth.js b/bigbluebutton-html5/imports/startup/client/auth.js
index bc4c15923e7f84347e637bc2876760413e1d0c3d..b87b1c735cc03aebf52220c8d6e4d266b883f483 100644
--- a/bigbluebutton-html5/imports/startup/client/auth.js
+++ b/bigbluebutton-html5/imports/startup/client/auth.js
@@ -35,9 +35,6 @@ export function logoutRouteHandler(nextState, replace) {
         protocolPattern.test(logoutURL) ?
           logoutURL :
           `http://${logoutURL}`;
-    })
-    .catch(() => {
-      replace({ pathname: '/error/500' });
     });
 }
 
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 94c8f38fd074425a6306cf10fb09f1bf293ba131..e81a75c6421494d370155c5ca0a8c4401b577515 100644
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
 import Auth from '/imports/ui/services/auth';
 import AppContainer from '/imports/ui/components/app/container';
 import ErrorScreen from '/imports/ui/components/error-screen/component';
+import MeetingEnded from '/imports/ui/components/meeting-ended/component';
 import LoadingScreen from '/imports/ui/components/loading-screen/component';
 import Settings from '/imports/ui/services/settings';
 import IntlStartup from './intl';
@@ -14,12 +15,14 @@ const propTypes = {
   errorCode: PropTypes.number,
   subscriptionsReady: PropTypes.bool.isRequired,
   locale: PropTypes.string,
+  endedCode: PropTypes.string,
 };
 
 const defaultProps = {
   error: undefined,
   errorCode: undefined,
   locale: undefined,
+  endedCode: undefined,
 };
 
 class Base extends Component {
@@ -54,6 +57,9 @@ class Base extends Component {
     const { loading, error } = this.state;
 
     const { subscriptionsReady, errorCode } = this.props;
+    const { endedCode } = this.props.params;
+
+    if (endedCode) return (<MeetingEnded code={endedCode} />);
 
     if (error || errorCode) {
       return (<ErrorScreen code={errorCode}>{error}</ErrorScreen>);
@@ -96,6 +102,7 @@ const BaseContainer = withRouter(withTracker(({ params, router }) => {
 
   const { credentials } = Auth;
 
+
   const subscriptionErrorHandler = {
     onError: (error) => {
       console.error(error);
@@ -106,6 +113,7 @@ const BaseContainer = withRouter(withTracker(({ params, router }) => {
   const subscriptionsHandlers = SUBSCRIPTIONS_NAME.map(name =>
     Meteor.subscribe(name, credentials, subscriptionErrorHandler));
 
+
   return {
     locale: Settings.application.locale,
     subscriptionsReady: subscriptionsHandlers.every(handler => handler.ready()),
diff --git a/bigbluebutton-html5/imports/startup/client/routes.js b/bigbluebutton-html5/imports/startup/client/routes.js
index d7b43af13718c98b5600847f6906fdb7e7612f4d..f396812ae71284a456c183fe58a3dc5347165621 100644
--- a/bigbluebutton-html5/imports/startup/client/routes.js
+++ b/bigbluebutton-html5/imports/startup/client/routes.js
@@ -5,7 +5,7 @@ import { createHistory } from 'history';
 import LoadingScreen from '/imports/ui/components/loading-screen/component';
 import ChatContainer from '/imports/ui/components/chat/container';
 import UserListContainer from '/imports/ui/components/user-list/container';
-
+import MeetingEnded from '/imports/ui/components/meeting-ended/component';
 import { joinRouteHandler, logoutRouteHandler, authenticatedRouteHandler } from './auth';
 import Base from './base';
 
@@ -35,6 +35,7 @@ const renderRoutes = () => (
       />
       <Redirect from="users/chat" to="/users/chat/public" />
     </Route>
+    <Route name="meeting-ended" path="/ended/:endedCode" component={Base} />
     <Route name="error" path="/error/:errorCode" component={Base} />
     <Redirect from="*" to="/error/404" />
   </Router>
diff --git a/bigbluebutton-html5/imports/startup/server/index.js b/bigbluebutton-html5/imports/startup/server/index.js
index 8d23b4450c5ca1a019e25ae5667c21751c4a74af..41ffed6aace191fd27a0fd0edfc08899abb68b51 100644
--- a/bigbluebutton-html5/imports/startup/server/index.js
+++ b/bigbluebutton-html5/imports/startup/server/index.js
@@ -1,9 +1,8 @@
 import { Meteor } from 'meteor/meteor';
+import Langmap from 'langmap';
+import fs from 'fs';
 import Logger from './logger';
 import Redis from './redis';
-import locales from '../../utils/locales';
-
-const availableLocales = [];
 
 Meteor.startup(() => {
   const APP_CONFIG = Meteor.settings.public.app;
@@ -31,7 +30,6 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
     normalizedLocale = `${localeRegion[0]}_${localeRegion[1].toUpperCase()}`;
     localeList.push(normalizedLocale);
   }
-
   localeList.forEach((locale) => {
     try {
       const data = Assets.getText(`locales/${locale}.json`);
@@ -47,15 +45,26 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
 });
 
 WebApp.connectHandlers.use('/locales', (req, res) => {
-  if (!availableLocales.length) {
-    locales.forEach((l) => {
-      try {
-        Assets.absoluteFilePath(`locales/${l.locale}.json`);
-        availableLocales.push(l);
-      } catch (e) {
-        // Getting here means the locale is not available on the files.
-      }
-    });
+  const APP_CONFIG = Meteor.settings.public.app;
+  const defaultLocale = APP_CONFIG.defaultSettings.application.locale;
+
+  let availableLocales = [];
+
+  const defaultLocaleFile = `${defaultLocale}.json`;
+  const defaultLocalePath = `locales/${defaultLocaleFile}`;
+  const localesPath = Assets.absoluteFilePath(defaultLocalePath).replace(defaultLocaleFile, '');
+
+  try {
+    const getAvailableLocales = fs.readdirSync(localesPath);
+    availableLocales = getAvailableLocales
+      .map(file => file.replace('.json', ''))
+      .map(file => file.replace('_', '-'))
+      .map(locale => ({
+        locale,
+        name: Langmap[locale].nativeName,
+      }));
+  } catch (e) {
+    // Getting here means the locale is not available on the files.
   }
 
   res.setHeader('Content-Type', 'application/json');
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
index 41d7800a78ba4db3de9d43a4e4fc411291aabf95..c0b37f152422e21e5ec2efd39e6b0046b45b8b81 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
@@ -54,6 +54,11 @@ class ActionsDropdown extends Component {
     this.handlePresentationClick = this.handlePresentationClick.bind(this);
   }
 
+  componentWillMount() {
+    this.presentationItemId = _.uniqueId('action-item-');
+    this.videoItemId = _.uniqueId('action-item-');
+  }
+
   componentWillUpdate(nextProps) {
     const { isUserPresenter: isPresenter } = nextProps;
     const { isUserPresenter: wasPresenter, mountModal } = this.props;
@@ -66,6 +71,34 @@ class ActionsDropdown extends Component {
     this.props.mountModal(<PresentationUploaderContainer />);
   }
 
+  getAvailableActions() {
+    const {
+      intl,
+      handleShareScreen,
+      handleUnshareScreen,
+      isVideoBroadcasting,
+    } = this.props;
+
+    return _.compact([
+      (<DropdownListItem
+        icon="presentation"
+        label={intl.formatMessage(intlMessages.presentationLabel)}
+        description={intl.formatMessage(intlMessages.presentationDesc)}
+        key={this.presentationItemId}
+        onClick={this.handlePresentationClick}
+      />),
+      (Meteor.settings.public.kurento.enableScreensharing ?
+        <DropdownListItem
+          icon="desktop"
+          label={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)}
+          description={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)}
+          key={this.videoItemId}
+          onClick={isVideoBroadcasting ? handleUnshareScreen : handleShareScreen }
+        />
+      : null),
+    ]);
+  }
+
   render() {
     const {
       intl,
@@ -75,6 +108,8 @@ class ActionsDropdown extends Component {
       isVideoBroadcasting,
     } = this.props;
 
+    const availableActions = this.getAvailableActions();
+
     if (!isUserPresenter) return null;
 
     return (
@@ -94,24 +129,7 @@ class ActionsDropdown extends Component {
         </DropdownTrigger>
         <DropdownContent placement="top left">
           <DropdownList>
-            <DropdownListItem
-              icon="presentation"
-              label={intl.formatMessage(intlMessages.presentationLabel)}
-              description={intl.formatMessage(intlMessages.presentationDesc)}
-              onClick={this.handlePresentationClick}
-            />
-            <DropdownListItem
-              icon="desktop"
-              label={intl.formatMessage(intlMessages.desktopShareLabel)}
-              description={intl.formatMessage(intlMessages.desktopShareDesc)}
-              onClick={handleShareScreen}
-            />
-            <DropdownListItem
-              icon="desktop"
-              label={intl.formatMessage(intlMessages.stopDesktopShareLabel)}
-              description={intl.formatMessage(intlMessages.stopDesktopShareDesc)}
-              onClick={handleUnshareScreen}
-            />
+            {availableActions}
           </DropdownList>
         </DropdownContent>
       </Dropdown>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 3d13ae4cffba0713e6af1ef9ab0a5ac28301ee3c..13381e57dcb707dc83825e65a55a2b42dc55e1ec 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -1,36 +1,53 @@
 import React from 'react';
+import cx from 'classnames';
 import { styles } from './styles.scss';
 import EmojiSelect from './emoji-select/component';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
 import JoinVideoOptionsContainer from '../video-dock/video-menu/container';
 
-const ActionsBar = ({
-  isUserPresenter,
-  handleExitVideo,
-  handleJoinVideo,
-  handleShareScreen,
-  handleUnshareScreen,
-  isVideoBroadcasting,
-  emojiList,
-  emojiSelected,
-  handleEmojiChange,
-}) => (
-  <div className={styles.actionsbar}>
-    <div className={styles.left}>
-      <ActionsDropdown {...{ isUserPresenter, handleShareScreen, handleUnshareScreen, isVideoBroadcasting}} />
-    </div>
-    <div className={styles.center}>
-      <AudioControlsContainer />
-      {Meteor.settings.public.kurento.enableVideo ?
-        <JoinVideoOptionsContainer
-          handleJoinVideo={handleJoinVideo}
-          handleCloseVideo={handleExitVideo}
-        />
-      : null}
-      <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} />
-    </div>
-  </div>
-);
+class ActionsBar extends React.PureComponent {
+  render() {
+    const {
+      isUserPresenter,
+      handleExitVideo,
+      handleJoinVideo,
+      handleShareScreen,
+      handleUnshareScreen,
+      isVideoBroadcasting,
+      emojiList,
+      emojiSelected,
+      handleEmojiChange,
+    } = this.props;
+
+    const actionBarClasses = {};
+    actionBarClasses[styles.centerWithActions] = isUserPresenter;
+    actionBarClasses[styles.center] = true;
+
+    return (
+      <div className={styles.actionsbar}>
+        <div className={styles.left}>
+          <ActionsDropdown {...{
+            isUserPresenter,
+            handleShareScreen,
+            handleUnshareScreen,
+            isVideoBroadcasting,
+            }}
+          />
+        </div>
+        <div className={isUserPresenter ? cx(styles.centerWithActions, actionBarClasses) : styles.center}>
+          <AudioControlsContainer />
+          {Meteor.settings.public.kurento.enableVideo ?
+            <JoinVideoOptionsContainer
+              handleJoinVideo={handleJoinVideo}
+              handleCloseVideo={handleExitVideo}
+            />
+            : null}
+          <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} />
+        </div>
+      </div>
+    );
+  }
+}
 
 export default ActionsBar;
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index a753c2cf520479cc08b2ae54f6a3fc0bffff0b72..6792290e4d0e9bc8f792c63efb66585740829053 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -16,6 +16,6 @@ export default withTracker(() => ({
   handleJoinVideo: () => VideoService.joinVideo(),
   handleShareScreen: () => shareScreen(),
   handleUnshareScreen: () => unshareScreen(),
-  isVideoBroadcasting: () => isVideoBroadcasting(),
+  isVideoBroadcasting: isVideoBroadcasting(),
 
 }))(ActionsBarContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index 968f74547b7783ae93a2f47f7de1c8b7cd883d0a..40d8d74bf344ec61f15f3619063525bf48db5191 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -13,16 +13,28 @@
   justify-content: center;
 
   > * {
-    margin: 0 $line-height-computed;
+    margin: 0 $sm-padding-x;
+
+    @include mq($small-only) {
+      margin: 0 $sm-padding-y;
+    }
   }
 }
 
 .left{
   position: absolute;
+  @include mq($small-only) {
+    bottom: $sm-padding-x;
+    left: $sm-padding-x;
+  }
 }
 
-.center {
-  align-items: center;
+.centerWithActions {
+  @include mq($xsmall-only) {
+    position: absolute;
+    bottom: $sm-padding-x;
+    right: $sm-padding-x;
+  }
 }
 
 .button {
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 05d1ef72c97b74cf553f50ccf155fa2eaedc468e..2963ce14ace6f4651309b3c9d1f146c71e7ffac5 100644
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -37,18 +37,10 @@ const defaultProps = {
 };
 
 const intlMessages = defineMessages({
-  kickedMessage: {
-    id: 'app.error.kicked',
-    description: 'Message when the user is removed from the conference',
-  },
   waitingApprovalMessage: {
     id: 'app.guest.waiting',
     description: 'Message while a guest is waiting to be approved',
   },
-  endMeetingMessage: {
-    id: 'app.error.meeting.ended',
-    description: 'You have logged out of the conference',
-  },
 });
 
 const AppContainer = (props) => {
@@ -72,6 +64,7 @@ const AppContainer = (props) => {
   );
 };
 
+
 export default withRouter(injectIntl(withModalMounter(withTracker(({ router, intl, baseControls }) => {
   const currentUser = Users.findOne({ userId: Auth.userID });
   const isMeetingBreakout = meetingIsBreakout();
@@ -80,7 +73,7 @@ export default withRouter(injectIntl(withModalMounter(withTracker(({ router, int
     baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
   }
 
-  // Displayed error messages according to the mode (kicked, end meeting)
+  // Displayed error messages according to the mode (removed, end meeting)
   const sendToError = (code, message) => {
     Auth.clearCredentials()
       .then(() => {
@@ -89,11 +82,11 @@ export default withRouter(injectIntl(withModalMounter(withTracker(({ router, int
       });
   };
 
-  // Check if user is kicked out of the session
+  // Check if user is removed out of the session
   Users.find({ userId: Auth.userID }).observeChanges({
     changed(id, fields) {
       if (fields.ejected) {
-        sendToError(403, intl.formatMessage(intlMessages.kickedMessage));
+        router.push(`/ended/${403}`);
       }
     },
   });
@@ -102,7 +95,7 @@ export default withRouter(injectIntl(withModalMounter(withTracker(({ router, int
   Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
     removed() {
       if (isMeetingBreakout) return;
-      sendToError(410, intl.formatMessage(intlMessages.endMeetingMessage));
+      router.push(`/ended/${410}`);
     },
   });
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
index 72150794ca8746b4307c97f723f37f7a931d40e3..043a7d7c2e66a375b89adb592b5b28d901daa7fd 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -5,7 +5,6 @@ import Button from '/imports/ui/components/button/component';
 import { styles } from './styles';
 import cx from 'classnames';
 
-
 const intlMessages = defineMessages({
   joinAudio: {
     id: 'app.audio.joinAudio',
@@ -34,6 +33,11 @@ const propTypes = {
   mute: PropTypes.bool.isRequired,
   join: PropTypes.bool.isRequired,
   intl: intlShape.isRequired,
+  glow: PropTypes.bool,
+};
+
+const defaultProps = {
+  glow: false,
 };
 
 const AudioControls = ({
@@ -56,9 +60,9 @@ const AudioControls = ({
         hideLabel
         label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
         aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
-        color={'primary'}
+        color="primary"
         icon={unmute ? 'mute' : 'unmute'}
-        size={'lg'}
+        size="lg"
         circle
       /> : null}
     <Button
@@ -70,11 +74,12 @@ const AudioControls = ({
       label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
       color={join ? 'danger' : 'primary'}
       icon={join ? 'audio_off' : 'audio_on'}
-      size={'lg'}
+      size="lg"
       circle
     />
   </span>);
 
 AudioControls.propTypes = propTypes;
+AudioControls.defaultProps = defaultProps;
 
 export default injectIntl(AudioControls);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
index 4b5d13f9fd037a273a45065fa787db35198a81e1..35e30706d1ea0fc851229a435e4c82ac68dbe210 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
@@ -1,9 +1,15 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
 .container {
   display: flex;
   flex-flow: row;
 
   > * {
-    margin: 0 1rem;
+    margin: 0 $sm-padding-x;
+
+    @include mq($small-only) {
+      margin: 0 $sm-padding-y;
+    }
 
     span:first-child {
       box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/component.jsx
deleted file mode 100644
index 717836bd01404a41ead99856dab6ef6038a10fa0..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/component.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-const propTypes = {
-  init: PropTypes.func.isRequired,
-};
-
-export default class Audio extends Component {
-  constructor(props) {
-    super(props);
-
-    this.init = props.init.bind(this);
-  }
-
-  componentDidMount() {
-    this.init();
-  }
-
-  render() {
-    return (
-      <audio id="remote-media" autoPlay="autoplay">
-        <track kind="captions" /> {/* These captions are brought to you by eslint */}
-      </audio>
-    );
-  }
-}
-
-Audio.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index 27371ff45c1b0d1d51c001b1815d39bcda4a942f..b950b9dca99b8f222f53b8b480486b95f9c18108 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -2,20 +2,10 @@ import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
 import { withModalMounter } from '/imports/ui/components/modal/service';
 import { injectIntl, defineMessages } from 'react-intl';
-import PropTypes from 'prop-types';
 import Breakouts from '/imports/api/breakouts';
 import Service from './service';
-import Audio from './component';
 import AudioModalContainer from './audio-modal/container';
 
-const propTypes = {
-  children: PropTypes.element,
-};
-
-const defaultProps = {
-  children: null,
-};
-
 const intlMessages = defineMessages({
   joinedAudio: {
     id: 'app.audioManager.joinedAudio',
@@ -52,7 +42,21 @@ const intlMessages = defineMessages({
 });
 
 
-const AudioContainer = props => <Audio {...props} />;
+class AudioContainer extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.init = props.init.bind(this);
+  }
+
+  componentDidMount() {
+    this.init();
+  }
+
+  render() {
+    return null;
+  }
+}
 
 let didMountAutoJoin = false;
 
@@ -96,6 +100,3 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) =>
     },
   };
 })(AudioContainer)));
-
-AudioContainer.propTypes = propTypes;
-AudioContainer.defaultProps = defaultProps;
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
index f03b3628dd5d6efe49e5470a6b8f2e5e8ec1de79..47917d833c4b20864eb82fc31bdafce8dda6c0c9 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
@@ -78,7 +78,8 @@ $dropdown-caret-height: 8px;
   font-size: $font-size-large * 1.1;
   width: calc(100% - #{($line-height-computed * 2)});
   left: $line-height-computed;
-  box-shadow: 0 0 0 2rem #fff;
+  box-shadow: 0 0 0 2rem $color-white !important;
+  border: $color-white !important;
 
   @include mq($small-only) {
     display: block;
diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
index 45a4295ee073f14430f071d4ab2665334371f6a5..d9cd49a84afa00bc0e2f586c1be33561b6f73181 100644
--- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
@@ -1,9 +1,9 @@
-import React, { Component } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import Button from '/imports/ui/components/button/component';
-
-import { styles } from './styles.scss';
+import { withRouter } from 'react-router';
+import { styles } from './styles';
 
 const intlMessages = defineMessages({
   500: {
@@ -17,9 +17,6 @@ const intlMessages = defineMessages({
   401: {
     id: 'app.error.401',
   },
-  403: {
-    id: 'app.error.403',
-  },
   leave: {
     id: 'app.error.leaveLabel',
     description: 'aria-label for leaving',
@@ -37,16 +34,13 @@ const defaultProps = {
   code: 500,
 };
 
-class ErrorScreen extends Component {
-
-  onClick() {
-    window.location = window.location.origin;
-  }
-
+class ErrorScreen extends React.PureComponent {
   render() {
-    const { intl, code, children } = this.props;
+    const {
+      intl, code, children, router,
+    } = this.props;
 
-    let formatedMessage = intl.formatMessage(intlMessages[500]);
+    let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]);
 
     if (code in intlMessages) {
       formatedMessage = intl.formatMessage(intlMessages[code]);
@@ -65,8 +59,8 @@ class ErrorScreen extends Component {
         </div>
         <div className={styles.content}>
           <Button
-            size={'sm'}
-            onClick={this.onClick}
+            size="sm"
+            onClick={() => router.push('/logout/')}
             label={intl.formatMessage(intlMessages.leave)}
           />
         </div>
@@ -75,7 +69,7 @@ class ErrorScreen extends Component {
   }
 }
 
-export default injectIntl(ErrorScreen);
+export default withRouter(injectIntl(ErrorScreen));
 
 ErrorScreen.propTypes = propTypes;
 ErrorScreen.defaultProps = defaultProps;
diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..09235a2cd5068409a143007a8c13ab1d66083130
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { withRouter } from 'react-router';
+import { defineMessages, injectIntl } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import { styles } from './styles';
+
+const intlMessage = defineMessages({
+  410: {
+    id: 'app.meeting.ended',
+    description: 'message when meeting is ended',
+  },
+  403: {
+    id: 'app.error.removed',
+    description: 'Message to display when user is removed from the conference',
+  },
+  messageEnded: {
+    id: 'app.meeting.endedMessage',
+    description: 'message saying to go back to home screen',
+  },
+  buttonOkay: {
+    id: 'app.meeting.endNotification.ok.label',
+    description: 'label okay for button',
+  },
+});
+
+const MeetingEnded = ({ intl, router, code }) => (
+  <div className={styles.parent}>
+    <div className={styles.modal}>
+      <div className={styles.content}>
+        <h1 className={styles.title}>{intl.formatMessage(intlMessage[code])}</h1>
+        <div className={styles.text}>
+          {intl.formatMessage(intlMessage.messageEnded)}
+        </div>
+        <Button
+          color="primary"
+          className={styles.button}
+          label={intl.formatMessage(intlMessage.buttonOkay)}
+          size="sm"
+          onClick={() => router.push('/logout')}
+        />
+      </div>
+    </div>
+  </div>
+);
+
+export default injectIntl(withRouter(MeetingEnded));
diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/styles.scss b/bigbluebutton-html5/imports/ui/components/meeting-ended/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..38bceeca1976079ac425f30a28169227fb60467f
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/styles.scss
@@ -0,0 +1,45 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.parent {
+  height: 100%;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.modal {
+  display: flex;
+  padding: $lg-padding-x;
+  background-color: $color-white;
+  flex-direction: column;
+  border-radius: $border-radius;
+  max-width: 95vw;
+  width: 600px;
+
+}
+
+.text {
+  color: $color-text;
+  font-weight: normal;
+  padding: $line-height-computed 0;
+  @include mq($small-only) {
+    font-size: $font-size-small;
+  };
+}
+
+.content {
+  text-align: center;
+}
+
+.title {
+  margin: 0;
+  font-size: $font-size-large;
+  font-weight: $headings-font-weight;
+}
+
+.button {
+  @include mq($small-only) {
+    font-size: $font-size-base;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss
index 07bcaebc3dab35c7bd39dee9f59bcc4c91ebd03e..972b67ef170d355c227bee3b098d24d1b87c1eb8 100644
--- a/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss
@@ -6,10 +6,12 @@
   align-self: flex-start;
   padding: ($line-height-computed / 2) $line-height-computed;
   outline: none;
+  @include mq($small-only) {
+    width: 100%
+  }
 }
 
 .content {
-  overflow: auto;
   color: $color-text;
   font-weight: normal;
   padding: $line-height-computed 0;
@@ -19,7 +21,6 @@
   display: flex;
   padding: $line-height-computed 0;
   border-bottom: $border-size solid $color-gray-lighter;
-  flex-shrink: 0;
 }
 
 .actions {
diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
index 956f0df0b07f60d7416214306f8ddc5d1474e46f..7ad3ac54d196889873a4d74c21a41692fd80d970 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
@@ -73,10 +73,12 @@ class Settings extends Component {
   constructor(props) {
     super(props);
 
-    const video = props.video;
-    const application = props.application;
-    const cc = props.cc;
-    const participants = props.participants;
+    const {
+      video,
+      participants,
+      cc,
+      application,
+    } = props;
 
     this.state = {
       current: {
@@ -128,9 +130,16 @@ class Settings extends Component {
         onSelect={this.handleSelectTab}
         selectedIndex={this.state.selectedTab}
         role="presentation"
+        selectedTabPanelClassName={styles.selectedTab}
       >
-        <TabList className={styles.tabList}>
-          <Tab className={styles.tabSelector} aria-labelledby="appTab">
+        <TabList
+          className={styles.tabList}
+        >
+          <Tab
+            className={styles.tabSelector}
+            aria-labelledby="appTab"
+            selectedClassName={styles.selected}
+          >
             <Icon iconName="application" className={styles.icon} />
             <span id="appTab">{intl.formatMessage(intlMessages.appTabLabel)}</span>
           </Tab>
@@ -138,7 +147,11 @@ class Settings extends Component {
           {/* <Icon iconName='video' className={styles.icon}/> */}
           {/* <span id="videoTab">{intl.formatMessage(intlMessages.videoTabLabel)}</span> */}
           {/* </Tab> */}
-          <Tab className={styles.tabSelector} aria-labelledby="ccTab">
+          <Tab
+            className={styles.tabSelector}
+            aria-labelledby="ccTab"
+            selectedClassName={styles.selected}
+          >
             <Icon iconName="user" className={styles.icon} />
             <span id="ccTab">{intl.formatMessage(intlMessages.closecaptionTabLabel)}</span>
           </Tab>
@@ -181,7 +194,7 @@ class Settings extends Component {
     );
   }
   render() {
-    const intl = this.props.intl;
+    const { intl } = this.props;
 
     return (
       <Modal
diff --git a/bigbluebutton-html5/imports/ui/components/settings/styles.scss b/bigbluebutton-html5/imports/ui/components/settings/styles.scss
index 27322cf43b2fb04d05e85208c1681eb3db0f974d..ba31c422dc33f516120ac7b9e3dca9f1891b97be 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/settings/styles.scss
@@ -4,55 +4,83 @@
   display: flex;
   flex-flow: row;
   justify-content: flex-start;
+  @include mq($small-only) {
+    width: 100%;
+    flex-flow: column;
+  }
 }
 
 .tabList {
   display: flex;
   flex-flow: column;
-  flex-grow: 0;
-  border: none !important;
-  flex-shrink: 0;
-  border-right: 0.2rem solid #d4d9df;
+  border: none;
+  padding: 0;
+  margin: 0;
+  width: calc(100% / 3);
+
+  @include mq($small-only) {
+    width: 100%;
+    flex-flow: row;
+    flex-wrap: wrap;
+    justify-content: center;
+  }
 }
 
 .icon {
-  margin-right: 0.7rem;
-  color: $color-gray-light;
+  margin-right: .5rem;
+  font-size: $font-size-large;
 }
 
 .tabSelector {
   font-size: 0.9rem;
-  padding: 0.1rem 0 !important;
-  padding-right: 1.2rem !important;
-  display: flex !important;
+  display: flex;
   flex-flow: row;
+  flex: 0 0 auto;
   justify-content: flex-start;
   border: none !important;
-  border-radius: 0 !important;
-  bottom: 0 !important;
-  margin: 0.5rem 0;
-  margin-right: 0.5rem;
-  transition: 0.3s color;
+  padding: $md-padding-y $md-padding-x;
   color: $color-gray-dark;
+  border-radius: .2rem;
+  cursor: pointer;
+  margin-bottom: $sm-padding-y;
+  align-items: center;
+  flex-grow: 0;
+  min-width: 0;
+  & > span {
+    @extend %text-elipsis;
+  }
 
-  &:first-child {
-    margin-top: 0;
+  @include mq($small-only) {
+    max-width: 100%;
+    margin-right: $sm-padding-x;
+    & > .icon {
+      display: none;
+    }
   }
+}
 
-  &[aria-selected="true"] {
-    border-right: 0.2rem solid $color-primary !important;
-    padding-right: 1rem !important;
-    color: $color-link !important;
+.selected {
+  color: $color-white;
+  background-color: $color-primary;
+  font-weight: bold;
 
-    &>.icon {
-      color: $color-link !important;
-    }
+  & > .icon {
+    color: $color-white;
   }
 }
 
 .tabPanel {
-  flex-grow: 1;
-  padding: 1rem;
-  padding-top: 0;
-  padding-right: 0;
+  display: none;
+  margin-left: 1rem;
+  width: calc(100% / 3 * 2);
+
+  @include mq($small-only) {
+    width: 100%;
+    margin-left: 0;
+    margin-top: 1rem;
+  }
+}
+
+.selectedTab {
+  display: block;
 }
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
index 00daa5eb17a19d10311dda6688d70bd8c9fa9813..330f874818d9b3d6bc21e9600b65c264c73d8748 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
@@ -23,7 +23,7 @@ const propTypes = {
   isPublicChat: PropTypes.func.isRequired,
   setEmojiStatus: PropTypes.func.isRequired,
   assignPresenter: PropTypes.func.isRequired,
-  kickUser: PropTypes.func.isRequired,
+  removeUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
@@ -63,7 +63,7 @@ class UserList extends Component {
           isBreakoutRoom={this.props.isBreakoutRoom}
           setEmojiStatus={this.props.setEmojiStatus}
           assignPresenter={this.props.assignPresenter}
-          kickUser={this.props.kickUser}
+          removeUser={this.props.removeUser}
           toggleVoice={this.props.toggleVoice}
           changeRole={this.props.changeRole}
           meeting={this.props.meeting}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
index 9242762db3534a0bac57c7a47244dec84f667686..af6165f73585a456bd1f6ea8ae50f2cdb5cc01e1 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
@@ -18,7 +18,7 @@ const propTypes = {
   isPublicChat: PropTypes.func.isRequired,
   setEmojiStatus: PropTypes.func.isRequired,
   assignPresenter: PropTypes.func.isRequired,
-  kickUser: PropTypes.func.isRequired,
+  removeUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
@@ -37,7 +37,7 @@ const UserListContainer = (props) => {
     isPublicChat,
     setEmojiStatus,
     assignPresenter,
-    kickUser,
+    removeUser,
     toggleVoice,
     changeRole,
     roving,
@@ -52,7 +52,7 @@ const UserListContainer = (props) => {
       isBreakoutRoom={isBreakoutRoom}
       setEmojiStatus={setEmojiStatus}
       assignPresenter={assignPresenter}
-      kickUser={kickUser}
+      removeUser={removeUser}
       toggleVoice={toggleVoice}
       changeRole={changeRole}
       getAvailableActions={getAvailableActions}
@@ -78,7 +78,7 @@ export default withTracker(({ params }) => ({
   isPublicChat: Service.isPublicChat,
   setEmojiStatus: Service.setEmojiStatus,
   assignPresenter: Service.assignPresenter,
-  kickUser: Service.kickUser,
+  removeUser: Service.removeUser,
   toggleVoice: Service.toggleVoice,
   changeRole: Service.changeRole,
   roving: Service.roving,
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 1116627ff613bc3a08bacf591457e32fcace4a58..9ad599475a17a0354cfddd1331426afaf35d62bb 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -239,8 +239,8 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
       && user.emoji.status !== EMOJI_STATUSES.none
       && !isDialInUser;
 
-  // if currentUser is a moderator, allow kicking other users
-  const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
+  // if currentUser is a moderator, allow removing other users
+  const allowedToRemove = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
 
   const allowedToSetPresenter = currentUser.isModerator
       && !user.isPresenter
@@ -261,7 +261,7 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
     allowedToMuteAudio,
     allowedToUnmuteAudio,
     allowedToResetStatus,
-    allowedToKick,
+    allowedToRemove,
     allowedToSetPresenter,
     allowedToPromote,
     allowedToDemote,
@@ -301,11 +301,11 @@ const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none');
 
 const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
 
-const kickUser = (userId) => {
+const removeUser = (userId) => {
   if (isVoiceOnlyUser(userId)) {
     makeCall('ejectUserFromVoice', userId);
   } else {
-    makeCall('kickUser', userId);
+    makeCall('removeUser', userId);
   }
 };
 
@@ -352,7 +352,7 @@ const roving = (event, itemCount, changeState) => {
 export default {
   setEmojiStatus,
   assignPresenter,
-  kickUser,
+  removeUser,
   toggleVoice,
   changeRole,
   getUsers,
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
index 4089ff461f530eef446cc23e10bb42872202e103..48c0a863730b4ba978c96353523f1e930078c035 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
@@ -20,7 +20,7 @@ const propTypes = {
   isPublicChat: PropTypes.func.isRequired,
   setEmojiStatus: PropTypes.func.isRequired,
   assignPresenter: PropTypes.func.isRequired,
-  kickUser: PropTypes.func.isRequired,
+  removeUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
@@ -54,7 +54,7 @@ class UserContent extends Component {
           isBreakoutRoom={this.props.isBreakoutRoom}
           setEmojiStatus={this.props.setEmojiStatus}
           assignPresenter={this.props.assignPresenter}
-          kickUser={this.props.kickUser}
+          removeUser={this.props.removeUser}
           toggleVoice={this.props.toggleVoice}
           changeRole={this.props.changeRole}
           getAvailableActions={this.props.getAvailableActions}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
index cf5c7121a0b99c27f3662ab3fb81373f910aeae9..f56c57859e11fcc04aa6d4566b26aece26008b24 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
@@ -17,7 +17,7 @@ const propTypes = {
   isBreakoutRoom: PropTypes.bool,
   setEmojiStatus: PropTypes.func.isRequired,
   assignPresenter: PropTypes.func.isRequired,
-  kickUser: PropTypes.func.isRequired,
+  removeUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   getAvailableActions: PropTypes.func.isRequired,
@@ -60,8 +60,8 @@ const intlMessages = defineMessages({
     id: 'app.userList.menu.makePresenter.label',
     description: 'Set this user to be the presenter in this meeting',
   },
-  KickUserLabel: {
-    id: 'app.userList.menu.kickUser.label',
+  RemoveUserLabel: {
+    id: 'app.userList.menu.removeUser.label',
     description: 'Forcefully remove this user from the meeting',
   },
   MuteUserAudioLabel: {
@@ -140,7 +140,7 @@ class UserParticipants extends Component {
       changeRole,
       assignPresenter,
       setEmojiStatus,
-      kickUser,
+      removeUser,
       toggleVoice,
     } = this.props;
 
@@ -161,9 +161,9 @@ class UserParticipants extends Component {
         handler: user => assignPresenter(user.id),
         icon: 'presentation',
       },
-      kick: {
-        label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
-        handler: user => kickUser(user.id),
+      remove: {
+        label: user => intl.formatMessage(intlMessages.RemoveUserLabel, { 0: user.name }),
+        handler: user => removeUser(user.id),
         icon: 'circle_close',
       },
       mute: {
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
index fe70a3f4a39eef557baaacc5bc2a803c9477af27..26d2c09dc9acf5647b53ce5f085359ab838b4be4 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
@@ -64,7 +64,7 @@ class UserListItem extends Component {
       openChat,
       clearStatus,
       setPresenter,
-      kick,
+      remove,
       mute,
       unmute,
       promote,
@@ -78,7 +78,7 @@ class UserListItem extends Component {
       allowedToMuteAudio,
       allowedToUnmuteAudio,
       allowedToResetStatus,
-      allowedToKick,
+      allowedToRemove,
       allowedToSetPresenter,
       allowedToPromote,
       allowedToDemote,
@@ -90,7 +90,7 @@ class UserListItem extends Component {
       (allowedToUnmuteAudio ? UserListItem.createAction(unmute, user) : null),
       (allowedToResetStatus ? UserListItem.createAction(clearStatus, user) : null),
       (allowedToSetPresenter ? UserListItem.createAction(setPresenter, user) : null),
-      (allowedToKick ? UserListItem.createAction(kick, user) : null),
+      (allowedToRemove ? UserListItem.createAction(remove, user) : null),
       (allowedToPromote ? UserListItem.createAction(promote, user) : null),
       (allowedToDemote ? UserListItem.createAction(demote, user) : null),
     ]);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
index a6ed22b5cdc0b34f65fb6b7d62ae20deaca6ea63..db8c606e23ff46f9d4477517ac12cb3e73537507 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
@@ -1,8 +1,34 @@
 import React, { Component } from 'react';
-import ScreenshareContainer from '/imports/ui/components/screenshare/container';
 import { styles } from './styles';
+import { defineMessages, injectIntl } from 'react-intl';
+import VideoService from './service';
 import { log } from '/imports/ui/services/api';
-
+import { notify } from '/imports/ui/services/notification';
+import { toast } from 'react-toastify';
+import Toast from '/imports/ui/components/toast/component';
+
+const intlMessages = defineMessages({
+  iceCandidateError: {
+    id: 'app.video.iceCandidateError',
+    description: 'Error message for ice candidate fail',
+  },
+  permissionError: {
+    id: 'app.video.permissionError',
+    description: 'Error message for webcam permission',
+  },
+  sharingError: {
+    id: 'app.video.sharingError',
+    description: 'Error on sharing webcam',
+  },
+  chromeExtensionError: {
+    id: 'app.video.chromeExtensionError',
+    description: 'Error message for Chrome Extension not installed',
+  },
+  chromeExtensionErrorLink: {
+    id: 'app.video.chromeExtensionErrorLink',
+    description: 'Error message for Chrome Extension not installed',
+  },
+});
 
 class VideoElement extends Component {
   constructor(props) {
@@ -18,7 +44,7 @@ class VideoElement extends Component {
   }
 }
 
-export default class VideoDock extends Component {
+class VideoDock extends Component {
   constructor(props) {
     super(props);
 
@@ -27,7 +53,7 @@ export default class VideoDock extends Component {
     this.wsQueue = [];
     this.webRtcPeers = {};
     this.reconnectWebcam = false;
-    this.reconnectList = false;
+    this.reconnectList = [];
     this.sharedCameraTimeout = null;
     this.subscribedCamerasTimeouts = [];
 
@@ -36,9 +62,6 @@ export default class VideoDock extends Component {
       sharedWebcam : false,
     };
 
-    this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this);
-    this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this);
-
     this.unshareWebcam = this.unshareWebcam.bind(this);
     this.shareWebcam = this.shareWebcam.bind(this);
 
@@ -78,17 +101,17 @@ export default class VideoDock extends Component {
 
   componentDidMount() {
     const ws = this.ws;
-    const { users } = this.props;
-    const id = users[0].userId;
+    const { users, userId } = this.props;
 
-    for (let i = 0; i < users.length; i++) {
-      if (users[i].has_stream && users[i].userId !== id) {
-        this.start(users[i].userId, false);
+    users.forEach((user) => {
+      if (user.has_stream && user.userId !== userId) {
+        this.start(user.userId, false);
       }
-    }
+    })
 
     document.addEventListener('joinVideo', this.shareWebcam.bind(this));// TODO find a better way to do this
     document.addEventListener('exitVideo', this.unshareWebcam.bind(this));
+    document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
 
     window.addEventListener('resize', this.adjustVideos);
 
@@ -98,22 +121,32 @@ export default class VideoDock extends Component {
   componentWillMount () {
     this.ws.addEventListener('open', this.onWsOpen);
     this.ws.addEventListener('close', this.onWsClose);
+
+    window.addEventListener('online', this.ws.open.bind(this.ws));
+    window.addEventListener('offline', this.ws.close.bind(this.ws));
   }
 
   componentWillUnmount () {
     document.removeEventListener('joinVideo', this.shareWebcam);
-    document.removeEventListener('exitVideo', this.shareWebcam);
+    document.removeEventListener('exitVideo', this.unshareWebcam);
+    document.removeEventListener('installChromeExtension', this.installChromeExtension);
     window.removeEventListener('resize', this.adjustVideos);
 
     this.ws.removeEventListener('message', this.onWsMessage);
     this.ws.removeEventListener('open', this.onWsOpen);
     this.ws.removeEventListener('close', this.onWsClose);
     // Close websocket connection to prevent multiple reconnects from happening
+
+    window.removeEventListener('online', this.ws.open);
+    window.removeEventListener('offline', this.ws.close);
+
     this.ws.close();
   }
 
   adjustVideos () {
-    window.adjustVideos('webcamArea', true);
+    setTimeout(() => {
+      window.adjustVideos('webcamArea', true);
+    }, 0);
   }
 
   onWsOpen () {
@@ -134,6 +167,7 @@ export default class VideoDock extends Component {
   }
 
   onWsMessage (msg) {
+    const { intl } = this.props;
     const parsedMessage = JSON.parse(msg.data);
 
     console.log('Received message new ws message: ');
@@ -166,6 +200,7 @@ export default class VideoDock extends Component {
           if (webRtcPeer.didSDPAnswered) {
             webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => {
               if (err) {
+                this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
                 return log('error', `Error adding candidate: ${err}`);
               }
             });
@@ -185,7 +220,9 @@ export default class VideoDock extends Component {
     console.log(`Starting video call for video: ${id} with ${shareWebcam}`);
 
     if (shareWebcam) {
+      VideoService.joiningVideo();
       this.setState({sharedWebcam: true});
+      this.myId = id;
       this.initWebRTC(id, true);
     } else {
       // initWebRTC with shareWebcam false will be called after react mounts the element
@@ -195,6 +232,7 @@ export default class VideoDock extends Component {
 
   initWebRTC(id, shareWebcam) {
     let that = this;
+    const { intl } = this.props;
 
     const onIceCandidate = function (candidate) {
       const message = {
@@ -241,10 +279,16 @@ export default class VideoDock extends Component {
     let webRtcPeer = new peerObj(options, function (error) {
       if (error) {
         log('error', ' WebRTC peerObj create error');
+        log('error', error);
+        that.notifyError(intl.formatMessage(intlMessages.permissionError));
+        /* This notification error is displayed considering kurento-utils 
+         * returned the error 'The request is not allowed by the user agent 
+         * or the platform in the current context.', but there are other
+         * errors that could be returned. */
 
         that.destroyWebRTCPeer(id);
         that.destroyVideoTag(id);
-
+        VideoService.resetState();
         return log('error', error);
       }
 
@@ -254,7 +298,6 @@ export default class VideoDock extends Component {
       that.webRtcPeers[id] = webRtcPeer;
       if (shareWebcam) {
         that.sharedWebcam = webRtcPeer;
-        that.myId = id;
       }
 
       this.generateOffer((error, offerSdp) => {
@@ -281,6 +324,7 @@ export default class VideoDock extends Component {
         let candidate = this.iceQueue.shift();
         this.addIceCandidate(candidate, (err) => {
           if (err) {
+            this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
             return console.error(`Error adding candidate: ${err}`);
           }
         });
@@ -301,10 +345,10 @@ export default class VideoDock extends Component {
   }
 
   stop(id) {
-    const { users } = this.props;
+    const { userId } = this.props;
     this.sendMessage({
       type: 'video',
-      role: id == users[0].userId ? 'share' : 'viewer',
+      role: id == userId ? 'share' : 'viewer',
       id: 'stop',
       cameraId: id,
     });
@@ -350,21 +394,21 @@ export default class VideoDock extends Component {
   }
 
   shareWebcam() {
-    const { users } = this.props;
-    const id = users[0].userId;
+    const { users, userId } = this.props;
 
     if (this.connectedToMediaServer()) {
-      this.start(id, true);
+      this.start(userId, true);
     } else {
-      log("error", "Not connected to media server BRA");
+      log("error", "Not connected to media server");
     }
   }
 
   unshareWebcam() {
+    VideoService.exitingVideo();
     log('info', 'Unsharing webcam');
-    const { users } = this.props;
-    const id = users[0].userId;
-    this.sendUserUnshareWebcam(id);
+    const { userId } = this.props;
+    VideoService.sendUserUnshareWebcam(userId);
+    VideoService.exitedVideo();
   }
 
   startResponse(message) {
@@ -387,7 +431,10 @@ export default class VideoDock extends Component {
       }
     });
 
-    this.sendUserShareWebcam(id);
+    if (message.cameraId == this.props.userId) {
+      log('info', "camera id sendusershare ", id);
+      VideoService.sendUserShareWebcam(id);
+    }
   }
 
   sendMessage(message) {
@@ -423,7 +470,7 @@ export default class VideoDock extends Component {
 
     const { users } = this.props;
 
-    if (message.cameraId == users[0].userId) {
+    if (message.cameraId == this.props) {
       this.unshareWebcam();
     } else {
       this.stop(message.cameraId);
@@ -432,13 +479,31 @@ export default class VideoDock extends Component {
 
   handlePlayStart(message) {
     log('info', 'Handle play start <===================');
+
+    if (message.cameraId == this.props.userId) {
+      VideoService.joinedVideo();
+    }
   }
 
   handleError(message) {
+    const { intl } = this.props;
+    this.notifyError(intl.formatMessage(intlMessages.sharingError));
+
     console.error(' Handle error --------------------->');
     log('debug', message.message);
   }
 
+  notifyError(message) {
+    notify(message, 'error', 'video');
+  }
+
+  installChromeExtension() {
+    const { intl } = this.props;
+    const CHROME_EXTENSION_LINK = Meteor.settings.public.kurento.chromeExtensionLink;
+
+    this.notifyError(<div>{intl.formatMessage(intlMessages.chromeExtensionError)} <a href={CHROME_EXTENSION_LINK} target="_blank">{intl.formatMessage(intlMessages.chromeExtensionErrorLink)}</a></div>);
+  }
+
   componentDidUpdate() {
     this.adjustVideos();
   }
@@ -466,9 +531,8 @@ export default class VideoDock extends Component {
   }
 
   shouldComponentUpdate(nextProps, nextState) {
-    const { users } = this.props;
+    const { users, userId } = this.props;
     const nextUsers = nextProps.users;
-    const id = users[0].userId;
 
     if (users) {
       let suc = false;
@@ -480,7 +544,7 @@ export default class VideoDock extends Component {
             console.log(`User ${nextUsers[i].has_stream ? '' : 'un'}shared webcam ${users[i].userId}`);
 
             if (nextUsers[i].has_stream) {
-              if (id !== users[i].userId) {
+              if (userId !== users[i].userId) {
                 this.start(users[i].userId, false);
               }
             } else {
@@ -501,4 +565,7 @@ export default class VideoDock extends Component {
 
     return false;
   }
+
 }
+
+export default injectIntl(VideoDock);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
index d4eb1e71401dbe23e0e8168771b5d9c32b0e81b5..81ec2f44cb649c0ff451195efa081564e073b5cd 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
@@ -10,7 +10,6 @@ const VideoDockContainer = ({ children, ...props }) => (
 );
 
 export default withTracker(() => ({
-  sendUserShareWebcam: VideoService.sendUserShareWebcam,
-  sendUserUnshareWebcam: VideoService.sendUserUnshareWebcam,
   users: VideoService.getAllUsers(),
+  userId: VideoService.userId(),
 }))(VideoDockContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/service.js
index 284b3a15b29ba39b1230674e507c7d522f663d3a..dbae7b46c163411d6104f29b0d3730f9b2186196 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/service.js
@@ -1,26 +1,109 @@
+import { Tracker } from 'meteor/tracker';
 import { makeCall } from '/imports/ui/services/api';
 import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
 
-const joinVideo = () => {
-  const joinVideoEvent = new Event('joinVideo');
-  document.dispatchEvent(joinVideoEvent);
-};
+class VideoService {
+  constructor() {
+    this.defineProperties({
+      isConnected: false,
+      isWaitingResponse: false,
+    });
+  }
 
-const exitVideo = () => {
-  const exitVideoEvent = new Event('exitVideo');
-  document.dispatchEvent(exitVideoEvent);
-};
+  defineProperties(obj) {
+    Object.keys(obj).forEach((key) => {
+      const privateKey = `_${key}`;
+      this[privateKey] = {
+        value: obj[key],
+        tracker: new Tracker.Dependency(),
+      };
 
-const sendUserShareWebcam = (stream) => {
-  makeCall('userShareWebcam', stream);
-};
+      Object.defineProperty(this, key, {
+        set: (value) => {
+          this[privateKey].value = value;
+          this[privateKey].tracker.changed();
+        },
+        get: () => {
+          this[privateKey].tracker.depend();
+          return this[privateKey].value;
+        },
+      });
+    });
+  }
 
-const sendUserUnshareWebcam = (stream) => {
-  makeCall('userUnshareWebcam', stream);
-};
+  joinVideo() {
+    var joinVideoEvent = new Event('joinVideo');
+    document.dispatchEvent(joinVideoEvent);
+  }
+
+  joiningVideo() {
+    this.isWaitingResponse = true;
+  }
+
+  joinedVideo() {
+    this.isWaitingResponse = false;
+    this.isConnected = true;
+  }
+
+  exitVideo() {
+    var exitVideoEvent = new Event('exitVideo');
+    document.dispatchEvent(exitVideoEvent);
+  }
+
+  exitingVideo() {
+    this.isWaitingResponse = true;
+  }
+
+  exitedVideo() {
+    this.isWaitingResponse = false;
+    this.isConnected = false;
+  }
+
+  resetState() {
+    this.isWaitingResponse = false;
+    this.isConnected = false;
+  }
+
+  sendUserShareWebcam(stream) {
+    makeCall('userShareWebcam', stream);
+  }
+
+  sendUserUnshareWebcam(stream) {
+    makeCall('userUnshareWebcam', stream);
+  }
+
+  getAllUsers() {
+    return Users.find().fetch();
+  }
+
+  userId() {
+    return Auth.userID;
+  }
+
+  isConnected() {
+    return this.isConnected;
+  }
+
+  isWaitingResponse() {
+    return this.isWaitingResponse;
+  }
+}
 
-const getAllUsers = () => Users.find().fetch();
+const videoService = new VideoService();
 
 export default {
-  sendUserShareWebcam, sendUserUnshareWebcam, joinVideo, exitVideo, getAllUsers,
+  exitVideo: () => videoService.exitVideo(),
+  exitingVideo: () => videoService.exitingVideo(),
+  exitedVideo: () => videoService.exitedVideo(),
+  getAllUsers: () => videoService.getAllUsers(),
+  isConnected: () => videoService.isConnected,
+  isWaitingResponse: () => videoService.isWaitingResponse,
+  joinVideo: () => videoService.joinVideo(),
+  joiningVideo: () => videoService.joiningVideo(),
+  joinedVideo: () => videoService.joinedVideo(),
+  resetState: () => videoService.resetState(),
+  sendUserShareWebcam: (stream) => videoService.sendUserShareWebcam(stream),
+  sendUserUnshareWebcam: (stream) => videoService.sendUserUnshareWebcam(stream),
+  userId: () => videoService.userId(),
 };
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
index 6d729bdbd319406377e4d5a4f90183d94f6c7bc7..cce42066cefcab79bebd56ac2b30b609812489a5 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
@@ -14,14 +14,7 @@ const intlMessages = defineMessages({
   },
 });
 
-const JoinVideoOptions = (props) => {
-  const {
-    intl,
-    isSharingVideo,
-    handleJoinVideo,
-    handleCloseVideo,
-  } = props;
-
+const JoinVideoOptions = ({intl, isWaitingResponse, isConnected, isSharingVideo, handleJoinVideo, handleCloseVideo}) => {
   if (isSharingVideo) {
     return (
       <Button
@@ -29,10 +22,11 @@ const JoinVideoOptions = (props) => {
         label={intl.formatMessage(intlMessages.leaveVideo)}
         hideLabel
         aria-label={intl.formatMessage(intlMessages.leaveVideo)}
-        color="danger"
-        icon="video"
-        size="lg"
+        color={'danger'}
+        icon={'video'}
+        size={'lg'}
         circle
+        disabled={isWaitingResponse}
       />
     );
   }
@@ -43,19 +37,13 @@ const JoinVideoOptions = (props) => {
       label={intl.formatMessage(intlMessages.joinVideo)}
       hideLabel
       aria-label={intl.formatMessage(intlMessages.joinVideo)}
-      color="primary"
-      icon="video_off"
-      size="lg"
+      color={'primary'}
+      icon={'video_off'}
+      size={'lg'}
       circle
+      disabled={isWaitingResponse || (!isSharingVideo && isConnected)}
     />
   );
-};
-
-JoinVideoOptions.propTypes = {
-  intl: intlShape.isRequired,
-  isSharingVideo: PropTypes.bool.isRequired,
-  handleJoinVideo: PropTypes.func.isRequired,
-  handleCloseVideo: PropTypes.func.isRequired,
-};
+}
 
 export default injectIntl(JoinVideoOptions);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
index e90d3e1799cc47e60604151ca304fb7838e5d147..793dd978cea382a746b66cabd66c78fc391b4ee8 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
@@ -2,13 +2,18 @@ import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
 import JoinVideoOptions from './component';
 import VideoMenuService from './service';
+import VideoService from '../service';
 
 const JoinVideoOptionsContainer = props => (<JoinVideoOptions {...props} />);
 
 export default withTracker((params) => {
   const isSharingVideo = VideoMenuService.isSharingVideo();
+  const isWaitingResponse = VideoService.isWaitingResponse();
+  const isConnected = VideoService.isConnected();
   return {
     isSharingVideo,
+    isWaitingResponse,
+    isConnected,
     handleJoinVideo: params.handleJoinVideo,
     handleCloseVideo: params.handleCloseVideo,
   };
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 6bab22deee57027321f94b3e51a063af57b471a7..06516e9580ef5c4d444646e4fee05b709800eae9 100644
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -32,15 +32,7 @@ class AudioManager {
       isWaitingPermissions: false,
       error: null,
       outputDeviceId: null,
-    });
-
-    const query = VoiceUsers.find({ intId: Auth.userID });
-
-    query.observeChanges({
-      changed: (id, fields) => {
-        if (fields.muted === this.isMuted) return;
-        this.isMuted = fields.muted;
-      },
+      muteHandle: null,
     });
   }
 
@@ -135,6 +127,17 @@ class AudioManager {
     this.isConnecting = false;
     this.isConnected = true;
 
+    // listen to the VoiceUsers changes and update the flag
+    if(!this.muteHandle) {
+      const query = VoiceUsers.find({ intId: Auth.userID });
+      this.muteHandle = query.observeChanges({
+        changed: (id, fields) => {
+          if (fields.muted === this.isMuted) return;
+          this.isMuted = fields.muted;
+        },
+      });
+    }
+
     if (!this.isEchoTest) {
       this.notify(this.messages.info.JOINED_AUDIO);
     }
diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js
index 92fa54f5dd0b676874385ae4054f7bc6f1f8435c..782a6df3856210f165d208c779a913080fd5ac44 100644
--- a/bigbluebutton-html5/imports/ui/services/auth/index.js
+++ b/bigbluebutton-html5/imports/ui/services/auth/index.js
@@ -111,23 +111,7 @@ class Auth {
     }
 
     return new Promise((resolve) => {
-      const credentialsSnapshot = {
-        meetingId: this.meetingID,
-        requesterUserId: this.userID,
-        requesterToken: this.token,
-      };
-
-      // make sure users who did not connect are not added to the meeting
-      // do **not** use the custom call - it relies on expired data
-      Meteor.call('userLogout', credentialsSnapshot, (error) => {
-        if (error) {
-          log('error', error, { credentials: credentialsSnapshot });
-        } else {
-          this.fetchLogoutUrl()
-            .then(this.clearCredentials)
-            .then(resolve);
-        }
-      });
+      resolve(this._logoutURL);
     });
   }
 
@@ -179,10 +163,6 @@ class Auth {
       makeCall('validateAuthToken');
     });
   }
-
-  fetchLogoutUrl() {
-    return Promise.resolve(this._logoutURL);
-  }
 }
 
 const AuthSingleton = new Auth();
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss
index 66e389d353b73481207035c5713e67f02cfaefe3..d21b00dd97ca148aefc2b13ad5d9de91c3148f19 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss
@@ -13,9 +13,10 @@
   @return nth($range, 2);
 }
 
-
-$small-range: (0em, 40em);
-/* 0, 640px */
+$xsmall-range: (0em, 25.937em);
+/* 0px, 415px */
+$small-range: (26em, 40em);
+/* 416px, 640px */
 $medium-range: (40.063em, 64em);
 /* 641px, 1024px */
 $large-range: (64.063em, 90em);
@@ -30,6 +31,7 @@ $landscape: "#{$screen} and (orientation: landscape)";
 $portrait: "#{$screen} and (orientation: portrait)";
 $small-up: $screen;
 $small-only: "#{$screen} and (max-width: #{upper-bound($small-range)})";
+$xsmall-only: "#{$screen} and (min-width:#{lower-bound($xsmall-range)}) and (max-width:#{upper-bound($xsmall-range)})";
 $medium-up: "#{$screen} and (min-width:#{lower-bound($medium-range)})";
 $medium-only: "#{$screen} and (min-width:#{lower-bound($medium-range)}) and (max-width:#{upper-bound($medium-range)})";
 $large-up: "#{$screen} and (min-width:#{lower-bound($large-range)})";
@@ -44,6 +46,7 @@ $breakpoints: (
   'landscape': $landscape,
   'portrait': $portrait,
   'small': $small-only,
+  'xsmall': $xsmall-only,
   'medium': $medium-only,
   'large': $large-only,
   'xlarge': $xlarge-only,
diff --git a/bigbluebutton-html5/imports/utils/locales.js b/bigbluebutton-html5/imports/utils/locales.js
deleted file mode 100644
index 6a66ee8be2ec5da0b7a29e4aba8e7cc24212bec0..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/utils/locales.js
+++ /dev/null
@@ -1,1740 +0,0 @@
-const locales = [
-  {
-    locale: 'af_NA',
-    name: 'Afrikaans (Namibia)',
-  },
-  {
-    locale: 'af_ZA',
-    name: 'Afrikaans (South Africa)',
-  },
-  {
-    locale: 'af',
-    name: 'Afrikaans',
-  },
-  {
-    locale: 'ak_GH',
-    name: 'Akan (Ghana)',
-  },
-  {
-    locale: 'ak',
-    name: 'Akan',
-  },
-  {
-    locale: 'sq_AL',
-    name: 'Albanian (Albania)',
-  },
-  {
-    locale: 'sq',
-    name: 'Albanian',
-  },
-  {
-    locale: 'am_ET',
-    name: 'Amharic (Ethiopia)',
-  },
-  {
-    locale: 'am',
-    name: 'Amharic',
-  },
-  {
-    locale: 'ar_DZ',
-    name: 'Arabic (Algeria)',
-  },
-  {
-    locale: 'ar_BH',
-    name: 'Arabic (Bahrain)',
-  },
-  {
-    locale: 'ar_EG',
-    name: 'Arabic (Egypt)',
-  },
-  {
-    locale: 'ar_IQ',
-    name: 'Arabic (Iraq)',
-  },
-  {
-    locale: 'ar_JO',
-    name: 'Arabic (Jordan)',
-  },
-  {
-    locale: 'ar_KW',
-    name: 'Arabic (Kuwait)',
-  },
-  {
-    locale: 'ar_LB',
-    name: 'Arabic (Lebanon)',
-  },
-  {
-    locale: 'ar_LY',
-    name: 'Arabic (Libya)',
-  },
-  {
-    locale: 'ar_MA',
-    name: 'Arabic (Morocco)',
-  },
-  {
-    locale: 'ar_OM',
-    name: 'Arabic (Oman)',
-  },
-  {
-    locale: 'ar_QA',
-    name: 'Arabic (Qatar)',
-  },
-  {
-    locale: 'ar_SA',
-    name: 'Arabic (Saudi Arabia)',
-  },
-  {
-    locale: 'ar_SD',
-    name: 'Arabic (Sudan)',
-  },
-  {
-    locale: 'ar_SY',
-    name: 'Arabic (Syria)',
-  },
-  {
-    locale: 'ar_TN',
-    name: 'Arabic (Tunisia)',
-  },
-  {
-    locale: 'ar_AE',
-    name: 'Arabic (United Arab Emirates)',
-  },
-  {
-    locale: 'ar_YE',
-    name: 'Arabic (Yemen)',
-  },
-  {
-    locale: 'ar',
-    name: 'Arabic',
-  },
-  {
-    locale: 'hy_AM',
-    name: 'Armenian (Armenia)',
-  },
-  {
-    locale: 'hy',
-    name: 'Armenian',
-  },
-  {
-    locale: 'as_IN',
-    name: 'Assamese (India)',
-  },
-  {
-    locale: 'as',
-    name: 'Assamese',
-  },
-  {
-    locale: 'asa_TZ',
-    name: 'Asu (Tanzania)',
-  },
-  {
-    locale: 'asa',
-    name: 'Asu',
-  },
-  {
-    locale: 'az_Cyrl',
-    name: 'Azerbaijani (Cyrillic)',
-  },
-  {
-    locale: 'az_Cyrl_AZ',
-    name: 'Azerbaijani (Cyrillic, Azerbaijan)',
-  },
-  {
-    locale: 'az_Latn',
-    name: 'Azerbaijani (Latin)',
-  },
-  {
-    locale: 'az_Latn_AZ',
-    name: 'Azerbaijani (Latin, Azerbaijan)',
-  },
-  {
-    locale: 'az',
-    name: 'Azerbaijani',
-  },
-  {
-    locale: 'bm_ML',
-    name: 'Bambara (Mali)',
-  },
-  {
-    locale: 'bm',
-    name: 'Bambara',
-  },
-  {
-    locale: 'eu_ES',
-    name: 'Basque (Spain)',
-  },
-  {
-    locale: 'eu',
-    name: 'Basque',
-  },
-  {
-    locale: 'be_BY',
-    name: 'Belarusian (Belarus)',
-  },
-  {
-    locale: 'be',
-    name: 'Belarusian',
-  },
-  {
-    locale: 'bem_ZM',
-    name: 'Bemba (Zambia)',
-  },
-  {
-    locale: 'bem',
-    name: 'Bemba',
-  },
-  {
-    locale: 'bez_TZ',
-    name: 'Bena (Tanzania)',
-  },
-  {
-    locale: 'bez',
-    name: 'Bena',
-  },
-  {
-    locale: 'bn_BD',
-    name: 'Bengali (Bangladesh)',
-  },
-  {
-    locale: 'bn_IN',
-    name: 'Bengali (India)',
-  },
-  {
-    locale: 'bn',
-    name: 'Bengali',
-  },
-  {
-    locale: 'bs_BA',
-    name: 'Bosnian (Bosnia and Herzegovina)',
-  },
-  {
-    locale: 'bs',
-    name: 'Bosnian',
-  },
-  {
-    locale: 'bg_BG',
-    name: 'Bulgarian (Bulgaria)',
-  },
-  {
-    locale: 'bg',
-    name: 'Bulgarian',
-  },
-  {
-    locale: 'my_MM',
-    name: 'Burmese (Myanmar [Burma])',
-  },
-  {
-    locale: 'my',
-    name: 'Burmese',
-  },
-  {
-    locale: 'ca_ES',
-    name: 'Catalan (Spain)',
-  },
-  {
-    locale: 'ca',
-    name: 'Catalan',
-  },
-  {
-    locale: 'tzm_Latn',
-    name: 'Central Morocco Tamazight (Latin)',
-  },
-  {
-    locale: 'tzm_Latn_MA',
-    name: 'Central Morocco Tamazight (Latin, Morocco)',
-  },
-  {
-    locale: 'tzm',
-    name: 'Central Morocco Tamazight',
-  },
-  {
-    locale: 'chr_US',
-    name: 'Cherokee (United States)',
-  },
-  {
-    locale: 'chr',
-    name: 'Cherokee',
-  },
-  {
-    locale: 'cgg_UG',
-    name: 'Chiga (Uganda)',
-  },
-  {
-    locale: 'cgg',
-    name: 'Chiga',
-  },
-  {
-    locale: 'zh_Hans',
-    name: 'Chinese (Simplified Han)',
-  },
-  {
-    locale: 'zh_Hans_CN',
-    name: 'Chinese (Simplified Han, China)',
-  },
-  {
-    locale: 'zh_Hans_HK',
-    name: 'Chinese (Simplified Han, Hong Kong SAR China)',
-  },
-  {
-    locale: 'zh_Hans_MO',
-    name: 'Chinese (Simplified Han, Macau SAR China)',
-  },
-  {
-    locale: 'zh_Hans_SG',
-    name: 'Chinese (Simplified Han, Singapore)',
-  },
-  {
-    locale: 'zh_Hant',
-    name: 'Chinese (Traditional Han)',
-  },
-  {
-    locale: 'zh_Hant_HK',
-    name: 'Chinese (Traditional Han, Hong Kong SAR China)',
-  },
-  {
-    locale: 'zh_Hant_MO',
-    name: 'Chinese (Traditional Han, Macau SAR China)',
-  },
-  {
-    locale: 'zh_Hant_TW',
-    name: 'Chinese (Traditional Han, Taiwan)',
-  },
-  {
-    locale: 'zh',
-    name: 'Chinese',
-  },
-  {
-    locale: 'kw_GB',
-    name: 'Cornish (United Kingdom)',
-  },
-  {
-    locale: 'kw',
-    name: 'Cornish',
-  },
-  {
-    locale: 'hr_HR',
-    name: 'Croatian (Croatia)',
-  },
-  {
-    locale: 'hr',
-    name: 'Croatian',
-  },
-  {
-    locale: 'cs_CZ',
-    name: 'Czech (Czech Republic)',
-  },
-  {
-    locale: 'cs',
-    name: 'Czech',
-  },
-  {
-    locale: 'da_DK',
-    name: 'Danish (Denmark)',
-  },
-  {
-    locale: 'da',
-    name: 'Danish',
-  },
-  {
-    locale: 'nl_BE',
-    name: 'Dutch (Belgium)',
-  },
-  {
-    locale: 'nl_NL',
-    name: 'Dutch (Netherlands)',
-  },
-  {
-    locale: 'nl',
-    name: 'Dutch',
-  },
-  {
-    locale: 'ebu_KE',
-    name: 'Embu (Kenya)',
-  },
-  {
-    locale: 'ebu',
-    name: 'Embu',
-  },
-  {
-    locale: 'en_AS',
-    name: 'English (American Samoa)',
-  },
-  {
-    locale: 'en_AU',
-    name: 'English (Australia)',
-  },
-  {
-    locale: 'en_BE',
-    name: 'English (Belgium)',
-  },
-  {
-    locale: 'en_BZ',
-    name: 'English (Belize)',
-  },
-  {
-    locale: 'en_BW',
-    name: 'English (Botswana)',
-  },
-  {
-    locale: 'en_CA',
-    name: 'English (Canada)',
-  },
-  {
-    locale: 'en_GU',
-    name: 'English (Guam)',
-  },
-  {
-    locale: 'en_HK',
-    name: 'English (Hong Kong SAR China)',
-  },
-  {
-    locale: 'en_IN',
-    name: 'English (India)',
-  },
-  {
-    locale: 'en_IE',
-    name: 'English (Ireland)',
-  },
-  {
-    locale: 'en_JM',
-    name: 'English (Jamaica)',
-  },
-  {
-    locale: 'en_MT',
-    name: 'English (Malta)',
-  },
-  {
-    locale: 'en_MH',
-    name: 'English (Marshall Islands)',
-  },
-  {
-    locale: 'en_MU',
-    name: 'English (Mauritius)',
-  },
-  {
-    locale: 'en_NA',
-    name: 'English (Namibia)',
-  },
-  {
-    locale: 'en_NZ',
-    name: 'English (New Zealand)',
-  },
-  {
-    locale: 'en_MP',
-    name: 'English (Northern Mariana Islands)',
-  },
-  {
-    locale: 'en_PK',
-    name: 'English (Pakistan)',
-  },
-  {
-    locale: 'en_PH',
-    name: 'English (Philippines)',
-  },
-  {
-    locale: 'en_SG',
-    name: 'English (Singapore)',
-  },
-  {
-    locale: 'en_ZA',
-    name: 'English (South Africa)',
-  },
-  {
-    locale: 'en_TT',
-    name: 'English (Trinidad and Tobago)',
-  },
-  {
-    locale: 'en_UM',
-    name: 'English (U.S. Minor Outlying Islands)',
-  },
-  {
-    locale: 'en_VI',
-    name: 'English (U.S. Virgin Islands)',
-  },
-  {
-    locale: 'en_GB',
-    name: 'English (United Kingdom)',
-  },
-  {
-    locale: 'en_US',
-    name: 'English (United States)',
-  },
-  {
-    locale: 'en_ZW',
-    name: 'English (Zimbabwe)',
-  },
-  {
-    locale: 'en',
-    name: 'English',
-  },
-  {
-    locale: 'eo',
-    name: 'Esperanto',
-  },
-  {
-    locale: 'et_EE',
-    name: 'Estonian (Estonia)',
-  },
-  {
-    locale: 'et',
-    name: 'Estonian',
-  },
-  {
-    locale: 'ee_GH',
-    name: 'Ewe (Ghana)',
-  },
-  {
-    locale: 'ee_TG',
-    name: 'Ewe (Togo)',
-  },
-  {
-    locale: 'ee',
-    name: 'Ewe',
-  },
-  {
-    locale: 'fo_FO',
-    name: 'Faroese (Faroe Islands)',
-  },
-  {
-    locale: 'fo',
-    name: 'Faroese',
-  },
-  {
-    locale: 'fil_PH',
-    name: 'Filipino (Philippines)',
-  },
-  {
-    locale: 'fil',
-    name: 'Filipino',
-  },
-  {
-    locale: 'fi_FI',
-    name: 'Finnish (Finland)',
-  },
-  {
-    locale: 'fi',
-    name: 'Finnish',
-  },
-  {
-    locale: 'fr_BE',
-    name: 'French (Belgium)',
-  },
-  {
-    locale: 'fr_BJ',
-    name: 'French (Benin)',
-  },
-  {
-    locale: 'fr_BF',
-    name: 'French (Burkina Faso)',
-  },
-  {
-    locale: 'fr_BI',
-    name: 'French (Burundi)',
-  },
-  {
-    locale: 'fr_CM',
-    name: 'French (Cameroon)',
-  },
-  {
-    locale: 'fr_CA',
-    name: 'French (Canada)',
-  },
-  {
-    locale: 'fr_CF',
-    name: 'French (Central African Republic)',
-  },
-  {
-    locale: 'fr_TD',
-    name: 'French (Chad)',
-  },
-  {
-    locale: 'fr_KM',
-    name: 'French (Comoros)',
-  },
-  {
-    locale: 'fr_CG',
-    name: 'French (Congo - Brazzaville)',
-  },
-  {
-    locale: 'fr_CD',
-    name: 'French (Congo - Kinshasa)',
-  },
-  {
-    locale: 'fr_CI',
-    name: 'French (Côte d’Ivoire)',
-  },
-  {
-    locale: 'fr_DJ',
-    name: 'French (Djibouti)',
-  },
-  {
-    locale: 'fr_GQ',
-    name: 'French (Equatorial Guinea)',
-  },
-  {
-    locale: 'fr_FR',
-    name: 'French (France)',
-  },
-  {
-    locale: 'fr_GA',
-    name: 'French (Gabon)',
-  },
-  {
-    locale: 'fr_GP',
-    name: 'French (Guadeloupe)',
-  },
-  {
-    locale: 'fr_GN',
-    name: 'French (Guinea)',
-  },
-  {
-    locale: 'fr_LU',
-    name: 'French (Luxembourg)',
-  },
-  {
-    locale: 'fr_MG',
-    name: 'French (Madagascar)',
-  },
-  {
-    locale: 'fr_ML',
-    name: 'French (Mali)',
-  },
-  {
-    locale: 'fr_MQ',
-    name: 'French (Martinique)',
-  },
-  {
-    locale: 'fr_MC',
-    name: 'French (Monaco)',
-  },
-  {
-    locale: 'fr_NE',
-    name: 'French (Niger)',
-  },
-  {
-    locale: 'fr_RW',
-    name: 'French (Rwanda)',
-  },
-  {
-    locale: 'fr_RE',
-    name: 'French (Réunion)',
-  },
-  {
-    locale: 'fr_BL',
-    name: 'French (Saint Barthélemy)',
-  },
-  {
-    locale: 'fr_MF',
-    name: 'French (Saint Martin)',
-  },
-  {
-    locale: 'fr_SN',
-    name: 'French (Senegal)',
-  },
-  {
-    locale: 'fr_CH',
-    name: 'French (Switzerland)',
-  },
-  {
-    locale: 'fr_TG',
-    name: 'French (Togo)',
-  },
-  {
-    locale: 'fr',
-    name: 'French',
-  },
-  {
-    locale: 'ff_SN',
-    name: 'Fulah (Senegal)',
-  },
-  {
-    locale: 'ff',
-    name: 'Fulah',
-  },
-  {
-    locale: 'gl_ES',
-    name: 'Galician (Spain)',
-  },
-  {
-    locale: 'gl',
-    name: 'Galician',
-  },
-  {
-    locale: 'lg_UG',
-    name: 'Ganda (Uganda)',
-  },
-  {
-    locale: 'lg',
-    name: 'Ganda',
-  },
-  {
-    locale: 'ka_GE',
-    name: 'Georgian (Georgia)',
-  },
-  {
-    locale: 'ka',
-    name: 'Georgian',
-  },
-  {
-    locale: 'de_AT',
-    name: 'German (Austria)',
-  },
-  {
-    locale: 'de_BE',
-    name: 'German (Belgium)',
-  },
-  {
-    locale: 'de_DE',
-    name: 'German (Germany)',
-  },
-  {
-    locale: 'de_LI',
-    name: 'German (Liechtenstein)',
-  },
-  {
-    locale: 'de_LU',
-    name: 'German (Luxembourg)',
-  },
-  {
-    locale: 'de_CH',
-    name: 'German (Switzerland)',
-  },
-  {
-    locale: 'de',
-    name: 'German',
-  },
-  {
-    locale: 'el_CY',
-    name: 'Greek (Cyprus)',
-  },
-  {
-    locale: 'el_GR',
-    name: 'Greek (Greece)',
-  },
-  {
-    locale: 'el',
-    name: 'Greek',
-  },
-  {
-    locale: 'gu_IN',
-    name: 'Gujarati (India)',
-  },
-  {
-    locale: 'gu',
-    name: 'Gujarati',
-  },
-  {
-    locale: 'guz_KE',
-    name: 'Gusii (Kenya)',
-  },
-  {
-    locale: 'guz',
-    name: 'Gusii',
-  },
-  {
-    locale: 'ha_Latn',
-    name: 'Hausa (Latin)',
-  },
-  {
-    locale: 'ha_Latn_GH',
-    name: 'Hausa (Latin, Ghana)',
-  },
-  {
-    locale: 'ha_Latn_NE',
-    name: 'Hausa (Latin, Niger)',
-  },
-  {
-    locale: 'ha_Latn_NG',
-    name: 'Hausa (Latin, Nigeria)',
-  },
-  {
-    locale: 'ha',
-    name: 'Hausa',
-  },
-  {
-    locale: 'haw_US',
-    name: 'Hawaiian (United States)',
-  },
-  {
-    locale: 'haw',
-    name: 'Hawaiian',
-  },
-  {
-    locale: 'he_IL',
-    name: 'Hebrew (Israel)',
-  },
-  {
-    locale: 'he',
-    name: 'Hebrew',
-  },
-  {
-    locale: 'hi_IN',
-    name: 'Hindi (India)',
-  },
-  {
-    locale: 'hi',
-    name: 'Hindi',
-  },
-  {
-    locale: 'hu_HU',
-    name: 'Hungarian (Hungary)',
-  },
-  {
-    locale: 'hu',
-    name: 'Hungarian',
-  },
-  {
-    locale: 'is_IS',
-    name: 'Icelandic (Iceland)',
-  },
-  {
-    locale: 'is',
-    name: 'Icelandic',
-  },
-  {
-    locale: 'ig_NG',
-    name: 'Igbo (Nigeria)',
-  },
-  {
-    locale: 'ig',
-    name: 'Igbo',
-  },
-  {
-    locale: 'id_ID',
-    name: 'Indonesian (Indonesia)',
-  },
-  {
-    locale: 'id',
-    name: 'Indonesian',
-  },
-  {
-    locale: 'ga_IE',
-    name: 'Irish (Ireland)',
-  },
-  {
-    locale: 'ga',
-    name: 'Irish',
-  },
-  {
-    locale: 'it_IT',
-    name: 'Italian (Italy)',
-  },
-  {
-    locale: 'it_CH',
-    name: 'Italian (Switzerland)',
-  },
-  {
-    locale: 'it',
-    name: 'Italian',
-  },
-  {
-    locale: 'ja_JP',
-    name: 'Japanese (Japan)',
-  },
-  {
-    locale: 'ja',
-    name: 'Japanese',
-  },
-  {
-    locale: 'kea_CV',
-    name: 'Kabuverdianu (Cape Verde)',
-  },
-  {
-    locale: 'kea',
-    name: 'Kabuverdianu',
-  },
-  {
-    locale: 'kab_DZ',
-    name: 'Kabyle (Algeria)',
-  },
-  {
-    locale: 'kab',
-    name: 'Kabyle',
-  },
-  {
-    locale: 'kl_GL',
-    name: 'Kalaallisut (Greenland)',
-  },
-  {
-    locale: 'kl',
-    name: 'Kalaallisut',
-  },
-  {
-    locale: 'kln_KE',
-    name: 'Kalenjin (Kenya)',
-  },
-  {
-    locale: 'kln',
-    name: 'Kalenjin',
-  },
-  {
-    locale: 'kam_KE',
-    name: 'Kamba (Kenya)',
-  },
-  {
-    locale: 'kam',
-    name: 'Kamba',
-  },
-  {
-    locale: 'kn_IN',
-    name: 'Kannada (India)',
-  },
-  {
-    locale: 'kn',
-    name: 'Kannada',
-  },
-  {
-    locale: 'kk_Cyrl',
-    name: 'Kazakh (Cyrillic)',
-  },
-  {
-    locale: 'kk_Cyrl_KZ',
-    name: 'Kazakh (Cyrillic, Kazakhstan)',
-  },
-  {
-    locale: 'kk',
-    name: 'Kazakh',
-  },
-  {
-    locale: 'km_KH',
-    name: 'Khmer (Cambodia)',
-  },
-  {
-    locale: 'km',
-    name: 'Khmer',
-  },
-  {
-    locale: 'ki_KE',
-    name: 'Kikuyu (Kenya)',
-  },
-  {
-    locale: 'ki',
-    name: 'Kikuyu',
-  },
-  {
-    locale: 'rw_RW',
-    name: 'Kinyarwanda (Rwanda)',
-  },
-  {
-    locale: 'rw',
-    name: 'Kinyarwanda',
-  },
-  {
-    locale: 'kok_IN',
-    name: 'Konkani (India)',
-  },
-  {
-    locale: 'kok',
-    name: 'Konkani',
-  },
-  {
-    locale: 'ko_KR',
-    name: 'Korean (South Korea)',
-  },
-  {
-    locale: 'ko',
-    name: 'Korean',
-  },
-  {
-    locale: 'khq_ML',
-    name: 'Koyra Chiini (Mali)',
-  },
-  {
-    locale: 'khq',
-    name: 'Koyra Chiini',
-  },
-  {
-    locale: 'ses_ML',
-    name: 'Koyraboro Senni (Mali)',
-  },
-  {
-    locale: 'ses',
-    name: 'Koyraboro Senni',
-  },
-  {
-    locale: 'lag_TZ',
-    name: 'Langi (Tanzania)',
-  },
-  {
-    locale: 'lag',
-    name: 'Langi',
-  },
-  {
-    locale: 'lv_LV',
-    name: 'Latvian (Latvia)',
-  },
-  {
-    locale: 'lv',
-    name: 'Latvian',
-  },
-  {
-    locale: 'lt_LT',
-    name: 'Lithuanian (Lithuania)',
-  },
-  {
-    locale: 'lt',
-    name: 'Lithuanian',
-  },
-  {
-    locale: 'luo_KE',
-    name: 'Luo (Kenya)',
-  },
-  {
-    locale: 'luo',
-    name: 'Luo',
-  },
-  {
-    locale: 'luy_KE',
-    name: 'Luyia (Kenya)',
-  },
-  {
-    locale: 'luy',
-    name: 'Luyia',
-  },
-  {
-    locale: 'mk_MK',
-    name: 'Macedonian (Macedonia)',
-  },
-  {
-    locale: 'mk',
-    name: 'Macedonian',
-  },
-  {
-    locale: 'jmc_TZ',
-    name: 'Machame (Tanzania)',
-  },
-  {
-    locale: 'jmc',
-    name: 'Machame',
-  },
-  {
-    locale: 'kde_TZ',
-    name: 'Makonde (Tanzania)',
-  },
-  {
-    locale: 'kde',
-    name: 'Makonde',
-  },
-  {
-    locale: 'mg_MG',
-    name: 'Malagasy (Madagascar)',
-  },
-  {
-    locale: 'mg',
-    name: 'Malagasy',
-  },
-  {
-    locale: 'ms_BN',
-    name: 'Malay (Brunei)',
-  },
-  {
-    locale: 'ms_MY',
-    name: 'Malay (Malaysia)',
-  },
-  {
-    locale: 'ms',
-    name: 'Malay',
-  },
-  {
-    locale: 'ml_IN',
-    name: 'Malayalam (India)',
-  },
-  {
-    locale: 'ml',
-    name: 'Malayalam',
-  },
-  {
-    locale: 'mt_MT',
-    name: 'Maltese (Malta)',
-  },
-  {
-    locale: 'mt',
-    name: 'Maltese',
-  },
-  {
-    locale: 'gv_GB',
-    name: 'Manx (United Kingdom)',
-  },
-  {
-    locale: 'gv',
-    name: 'Manx',
-  },
-  {
-    locale: 'mr_IN',
-    name: 'Marathi (India)',
-  },
-  {
-    locale: 'mr',
-    name: 'Marathi',
-  },
-  {
-    locale: 'mas_KE',
-    name: 'Masai (Kenya)',
-  },
-  {
-    locale: 'mas_TZ',
-    name: 'Masai (Tanzania)',
-  },
-  {
-    locale: 'mas',
-    name: 'Masai',
-  },
-  {
-    locale: 'mer_KE',
-    name: 'Meru (Kenya)',
-  },
-  {
-    locale: 'mer',
-    name: 'Meru',
-  },
-  {
-    locale: 'mfe_MU',
-    name: 'Morisyen (Mauritius)',
-  },
-  {
-    locale: 'mfe',
-    name: 'Morisyen',
-  },
-  {
-    locale: 'naq_NA',
-    name: 'Nama (Namibia)',
-  },
-  {
-    locale: 'naq',
-    name: 'Nama',
-  },
-  {
-    locale: 'ne_IN',
-    name: 'Nepali (India)',
-  },
-  {
-    locale: 'ne_NP',
-    name: 'Nepali (Nepal)',
-  },
-  {
-    locale: 'ne',
-    name: 'Nepali',
-  },
-  {
-    locale: 'nd_ZW',
-    name: 'North Ndebele (Zimbabwe)',
-  },
-  {
-    locale: 'nd',
-    name: 'North Ndebele',
-  },
-  {
-    locale: 'nb_NO',
-    name: 'Norwegian Bokmål (Norway)',
-  },
-  {
-    locale: 'nb',
-    name: 'Norwegian Bokmål',
-  },
-  {
-    locale: 'nn_NO',
-    name: 'Norwegian Nynorsk (Norway)',
-  },
-  {
-    locale: 'nn',
-    name: 'Norwegian Nynorsk',
-  },
-  {
-    locale: 'nyn_UG',
-    name: 'Nyankole (Uganda)',
-  },
-  {
-    locale: 'nyn',
-    name: 'Nyankole',
-  },
-  {
-    locale: 'or_IN',
-    name: 'Oriya (India)',
-  },
-  {
-    locale: 'or',
-    name: 'Oriya',
-  },
-  {
-    locale: 'om_ET',
-    name: 'Oromo (Ethiopia)',
-  },
-  {
-    locale: 'om_KE',
-    name: 'Oromo (Kenya)',
-  },
-  {
-    locale: 'om',
-    name: 'Oromo',
-  },
-  {
-    locale: 'ps_AF',
-    name: 'Pashto (Afghanistan)',
-  },
-  {
-    locale: 'ps',
-    name: 'Pashto',
-  },
-  {
-    locale: 'fa_AF',
-    name: 'Persian (Afghanistan)',
-  },
-  {
-    locale: 'fa_IR',
-    name: 'Persian (Iran)',
-  },
-  {
-    locale: 'fa',
-    name: 'Persian',
-  },
-  {
-    locale: 'pl_PL',
-    name: 'Polish (Poland)',
-  },
-  {
-    locale: 'pl',
-    name: 'Polish',
-  },
-  {
-    locale: 'pt_BR',
-    name: 'Portuguese (Brazil)',
-  },
-  {
-    locale: 'pt_GW',
-    name: 'Portuguese (Guinea-Bissau)',
-  },
-  {
-    locale: 'pt_MZ',
-    name: 'Portuguese (Mozambique)',
-  },
-  {
-    locale: 'pt_PT',
-    name: 'Portuguese (Portugal)',
-  },
-  {
-    locale: 'pt',
-    name: 'Portuguese',
-  },
-  {
-    locale: 'pa_Arab',
-    name: 'Punjabi (Arabic)',
-  },
-  {
-    locale: 'pa_Arab_PK',
-    name: 'Punjabi (Arabic, Pakistan)',
-  },
-  {
-    locale: 'pa_Guru',
-    name: 'Punjabi (Gurmukhi)',
-  },
-  {
-    locale: 'pa_Guru_IN',
-    name: 'Punjabi (Gurmukhi, India)',
-  },
-  {
-    locale: 'pa',
-    name: 'Punjabi',
-  },
-  {
-    locale: 'ro_MD',
-    name: 'Romanian (Moldova)',
-  },
-  {
-    locale: 'ro_RO',
-    name: 'Romanian (Romania)',
-  },
-  {
-    locale: 'ro',
-    name: 'Romanian',
-  },
-  {
-    locale: 'rm_CH',
-    name: 'Romansh (Switzerland)',
-  },
-  {
-    locale: 'rm',
-    name: 'Romansh',
-  },
-  {
-    locale: 'rof_TZ',
-    name: 'Rombo (Tanzania)',
-  },
-  {
-    locale: 'rof',
-    name: 'Rombo',
-  },
-  {
-    locale: 'ru_MD',
-    name: 'Russian (Moldova)',
-  },
-  {
-    locale: 'ru_RU',
-    name: 'Russian (Russia)',
-  },
-  {
-    locale: 'ru_UA',
-    name: 'Russian (Ukraine)',
-  },
-  {
-    locale: 'ru',
-    name: 'Russian',
-  },
-  {
-    locale: 'rwk_TZ',
-    name: 'Rwa (Tanzania)',
-  },
-  {
-    locale: 'rwk',
-    name: 'Rwa',
-  },
-  {
-    locale: 'saq_KE',
-    name: 'Samburu (Kenya)',
-  },
-  {
-    locale: 'saq',
-    name: 'Samburu',
-  },
-  {
-    locale: 'sg_CF',
-    name: 'Sango (Central African Republic)',
-  },
-  {
-    locale: 'sg',
-    name: 'Sango',
-  },
-  {
-    locale: 'seh_MZ',
-    name: 'Sena (Mozambique)',
-  },
-  {
-    locale: 'seh',
-    name: 'Sena',
-  },
-  {
-    locale: 'sr_Cyrl',
-    name: 'Serbian (Cyrillic)',
-  },
-  {
-    locale: 'sr_Cyrl_BA',
-    name: 'Serbian (Cyrillic, Bosnia and Herzegovina)',
-  },
-  {
-    locale: 'sr_Cyrl_ME',
-    name: 'Serbian (Cyrillic, Montenegro)',
-  },
-  {
-    locale: 'sr_Cyrl_RS',
-    name: 'Serbian (Cyrillic, Serbia)',
-  },
-  {
-    locale: 'sr_Latn',
-    name: 'Serbian (Latin)',
-  },
-  {
-    locale: 'sr_Latn_BA',
-    name: 'Serbian (Latin, Bosnia and Herzegovina)',
-  },
-  {
-    locale: 'sr_Latn_ME',
-    name: 'Serbian (Latin, Montenegro)',
-  },
-  {
-    locale: 'sr_Latn_RS',
-    name: 'Serbian (Latin, Serbia)',
-  },
-  {
-    locale: 'sr',
-    name: 'Serbian',
-  },
-  {
-    locale: 'sn_ZW',
-    name: 'Shona (Zimbabwe)',
-  },
-  {
-    locale: 'sn',
-    name: 'Shona',
-  },
-  {
-    locale: 'ii_CN',
-    name: 'Sichuan Yi (China)',
-  },
-  {
-    locale: 'ii',
-    name: 'Sichuan Yi',
-  },
-  {
-    locale: 'si_LK',
-    name: 'Sinhala (Sri Lanka)',
-  },
-  {
-    locale: 'si',
-    name: 'Sinhala',
-  },
-  {
-    locale: 'sk_SK',
-    name: 'Slovak (Slovakia)',
-  },
-  {
-    locale: 'sk',
-    name: 'Slovak',
-  },
-  {
-    locale: 'sl_SI',
-    name: 'Slovenian (Slovenia)',
-  },
-  {
-    locale: 'sl',
-    name: 'Slovenian',
-  },
-  {
-    locale: 'xog_UG',
-    name: 'Soga (Uganda)',
-  },
-  {
-    locale: 'xog',
-    name: 'Soga',
-  },
-  {
-    locale: 'so_DJ',
-    name: 'Somali (Djibouti)',
-  },
-  {
-    locale: 'so_ET',
-    name: 'Somali (Ethiopia)',
-  },
-  {
-    locale: 'so_KE',
-    name: 'Somali (Kenya)',
-  },
-  {
-    locale: 'so_SO',
-    name: 'Somali (Somalia)',
-  },
-  {
-    locale: 'so',
-    name: 'Somali',
-  },
-  {
-    locale: 'es_AR',
-    name: 'Spanish (Argentina)',
-  },
-  {
-    locale: 'es_BO',
-    name: 'Spanish (Bolivia)',
-  },
-  {
-    locale: 'es_CL',
-    name: 'Spanish (Chile)',
-  },
-  {
-    locale: 'es_CO',
-    name: 'Spanish (Colombia)',
-  },
-  {
-    locale: 'es_CR',
-    name: 'Spanish (Costa Rica)',
-  },
-  {
-    locale: 'es_DO',
-    name: 'Spanish (Dominican Republic)',
-  },
-  {
-    locale: 'es_EC',
-    name: 'Spanish (Ecuador)',
-  },
-  {
-    locale: 'es_SV',
-    name: 'Spanish (El Salvador)',
-  },
-  {
-    locale: 'es_GQ',
-    name: 'Spanish (Equatorial Guinea)',
-  },
-  {
-    locale: 'es_GT',
-    name: 'Spanish (Guatemala)',
-  },
-  {
-    locale: 'es_HN',
-    name: 'Spanish (Honduras)',
-  },
-  {
-    locale: 'es_419',
-    name: 'Spanish (Latin America)',
-  },
-  {
-    locale: 'es_MX',
-    name: 'Spanish (Mexico)',
-  },
-  {
-    locale: 'es_NI',
-    name: 'Spanish (Nicaragua)',
-  },
-  {
-    locale: 'es_PA',
-    name: 'Spanish (Panama)',
-  },
-  {
-    locale: 'es_PY',
-    name: 'Spanish (Paraguay)',
-  },
-  {
-    locale: 'es_PE',
-    name: 'Spanish (Peru)',
-  },
-  {
-    locale: 'es_PR',
-    name: 'Spanish (Puerto Rico)',
-  },
-  {
-    locale: 'es_ES',
-    name: 'Spanish (Spain)',
-  },
-  {
-    locale: 'es_US',
-    name: 'Spanish (United States)',
-  },
-  {
-    locale: 'es_UY',
-    name: 'Spanish (Uruguay)',
-  },
-  {
-    locale: 'es_VE',
-    name: 'Spanish (Venezuela)',
-  },
-  {
-    locale: 'es',
-    name: 'Spanish',
-  },
-  {
-    locale: 'sw_KE',
-    name: 'Swahili (Kenya)',
-  },
-  {
-    locale: 'sw_TZ',
-    name: 'Swahili (Tanzania)',
-  },
-  {
-    locale: 'sw',
-    name: 'Swahili',
-  },
-  {
-    locale: 'sv_FI',
-    name: 'Swedish (Finland)',
-  },
-  {
-    locale: 'sv_SE',
-    name: 'Swedish (Sweden)',
-  },
-  {
-    locale: 'sv',
-    name: 'Swedish',
-  },
-  {
-    locale: 'gsw_CH',
-    name: 'Swiss German (Switzerland)',
-  },
-  {
-    locale: 'gsw',
-    name: 'Swiss German',
-  },
-  {
-    locale: 'shi_Latn',
-    name: 'Tachelhit (Latin)',
-  },
-  {
-    locale: 'shi_Latn_MA',
-    name: 'Tachelhit (Latin, Morocco)',
-  },
-  {
-    locale: 'shi_Tfng',
-    name: 'Tachelhit (Tifinagh)',
-  },
-  {
-    locale: 'shi_Tfng_MA',
-    name: 'Tachelhit (Tifinagh, Morocco)',
-  },
-  {
-    locale: 'shi',
-    name: 'Tachelhit',
-  },
-  {
-    locale: 'dav_KE',
-    name: 'Taita (Kenya)',
-  },
-  {
-    locale: 'dav',
-    name: 'Taita',
-  },
-  {
-    locale: 'ta_IN',
-    name: 'Tamil (India)',
-  },
-  {
-    locale: 'ta_LK',
-    name: 'Tamil (Sri Lanka)',
-  },
-  {
-    locale: 'ta',
-    name: 'Tamil',
-  },
-  {
-    locale: 'te_IN',
-    name: 'Telugu (India)',
-  },
-  {
-    locale: 'te',
-    name: 'Telugu',
-  },
-  {
-    locale: 'teo_KE',
-    name: 'Teso (Kenya)',
-  },
-  {
-    locale: 'teo_UG',
-    name: 'Teso (Uganda)',
-  },
-  {
-    locale: 'teo',
-    name: 'Teso',
-  },
-  {
-    locale: 'th_TH',
-    name: 'Thai (Thailand)',
-  },
-  {
-    locale: 'th',
-    name: 'Thai',
-  },
-  {
-    locale: 'bo_CN',
-    name: 'Tibetan (China)',
-  },
-  {
-    locale: 'bo_IN',
-    name: 'Tibetan (India)',
-  },
-  {
-    locale: 'bo',
-    name: 'Tibetan',
-  },
-  {
-    locale: 'ti_ER',
-    name: 'Tigrinya (Eritrea)',
-  },
-  {
-    locale: 'ti_ET',
-    name: 'Tigrinya (Ethiopia)',
-  },
-  {
-    locale: 'ti',
-    name: 'Tigrinya',
-  },
-  {
-    locale: 'to_TO',
-    name: 'Tonga (Tonga)',
-  },
-  {
-    locale: 'to',
-    name: 'Tonga',
-  },
-  {
-    locale: 'tr_TR',
-    name: 'Turkish (Turkey)',
-  },
-  {
-    locale: 'tr',
-    name: 'Turkish',
-  },
-  {
-    locale: 'uk_UA',
-    name: 'Ukrainian (Ukraine)',
-  },
-  {
-    locale: 'uk',
-    name: 'Ukrainian',
-  },
-  {
-    locale: 'ur_IN',
-    name: 'Urdu (India)',
-  },
-  {
-    locale: 'ur_PK',
-    name: 'Urdu (Pakistan)',
-  },
-  {
-    locale: 'ur',
-    name: 'Urdu',
-  },
-  {
-    locale: 'uz_Arab',
-    name: 'Uzbek (Arabic)',
-  },
-  {
-    locale: 'uz_Arab_AF',
-    name: 'Uzbek (Arabic, Afghanistan)',
-  },
-  {
-    locale: 'uz_Cyrl',
-    name: 'Uzbek (Cyrillic)',
-  },
-  {
-    locale: 'uz_Cyrl_UZ',
-    name: 'Uzbek (Cyrillic, Uzbekistan)',
-  },
-  {
-    locale: 'uz_Latn',
-    name: 'Uzbek (Latin)',
-  },
-  {
-    locale: 'uz_Latn_UZ',
-    name: 'Uzbek (Latin, Uzbekistan)',
-  },
-  {
-    locale: 'uz',
-    name: 'Uzbek',
-  },
-  {
-    locale: 'vi_VN',
-    name: 'Vietnamese (Vietnam)',
-  },
-  {
-    locale: 'vi',
-    name: 'Vietnamese',
-  },
-  {
-    locale: 'vun_TZ',
-    name: 'Vunjo (Tanzania)',
-  },
-  {
-    locale: 'vun',
-    name: 'Vunjo',
-  },
-  {
-    locale: 'cy_GB',
-    name: 'Welsh (United Kingdom)',
-  },
-  {
-    locale: 'cy',
-    name: 'Welsh',
-  },
-  {
-    locale: 'yo_NG',
-    name: 'Yoruba (Nigeria)',
-  },
-  {
-    locale: 'yo',
-    name: 'Yoruba',
-  },
-  {
-    locale: 'zu_ZA',
-    name: 'Zulu (South Africa)',
-  },
-  {
-    locale: 'zu',
-    name: 'Zulu',
-  },
-];
-
-export default locales;
diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json
index fc95b44e923a100b80c2247bf523c0482bf375ff..5b77efb216b1cec2b28f7237010e2020adf99eb2 100644
--- a/bigbluebutton-html5/package.json
+++ b/bigbluebutton-html5/package.json
@@ -33,6 +33,7 @@
     "hiredis": "~0.5.0",
     "history": "~3.3.0",
     "immutability-helper": "~2.4.0",
+    "langmap": "0.0.16",
     "lodash": "~4.17.4",
     "meteor-node-stubs": "~0.3.2",
     "node-sass": "~4.5.3",
diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json
index 8a5c78070f2d604bb7cce93e8d36b123a3b4e804..aa28943454ccf1663252a415c7d985ca6b681f0c 100644
--- a/bigbluebutton-html5/private/config/settings-development.json
+++ b/bigbluebutton-html5/private/config/settings-development.json
@@ -9,8 +9,8 @@
       "listenOnly": false,
       "skipCheck": false,
       "appName": "BigBlueButton HTML5 Client",
-      "bbbServerVersion": "2.0-beta",
-      "copyright": "©2017 BigBlueButton Inc.",
+      "bbbServerVersion": "2.0-rc",
+      "copyright": "©2018 BigBlueButton Inc.",
       "html5ClientBuild": "HTML5_CLIENT_VERSION",
       "lockOnJoin": true,
       "basename": "/html5client",
@@ -88,7 +88,7 @@
       "moderator": {
         "methods": [
           "assignPresenter",
-          "kickUser",
+          "removeUser",
           "muteUser",
           "unmuteUser",
           "endMeeting",
diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json
index 34a90db20dfeda59f133aa353879537b3fe6f441..3f826e6cc0a725f19237ee05f7b5a0524ff22c56 100644
--- a/bigbluebutton-html5/private/config/settings-production.json
+++ b/bigbluebutton-html5/private/config/settings-production.json
@@ -9,8 +9,8 @@
       "listenOnly": false,
       "skipCheck": false,
       "appName": "BigBlueButton HTML5 Client",
-      "bbbServerVersion": "2.0-beta",
-      "copyright": "©2017 BigBlueButton Inc.",
+      "bbbServerVersion": "2.0-rc",
+      "copyright": "©2018 BigBlueButton Inc.",
       "html5ClientBuild": "HTML5_CLIENT_VERSION",
       "lockOnJoin": true,
       "basename": "/html5client",
@@ -88,7 +88,7 @@
       "moderator": {
         "methods": [
           "assignPresenter",
-          "kickUser",
+          "removeUser",
           "muteUser",
           "unmuteUser",
           "endMeeting",
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 609699ea9fdf09b8f30004680fcbf12598a54252..baf7415486c23908e125a02519348132d499db34 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -33,13 +33,15 @@
     "app.userList.menu.chat.label": "Chat",
     "app.userList.menu.clearStatus.label": "Clear Status",
     "app.userList.menu.makePresenter.label": "Make Presenter",
-    "app.userList.menu.kickUser.label": "Kick user",
+    "app.userList.menu.removeUser.label": "Remove user",
     "app.userList.menu.muteUserAudio.label": "Mute user",
     "app.userList.menu.unmuteUserAudio.label": "Unmute user",
     "app.userList.userAriaLabel": "User : {0}  Role: {1}  Person: {2}  Status: {3}",
     "app.userList.menu.promoteUser.label": "Promote {0} to moderator",
     "app.userList.menu.demoteUser.label": "Demote {0} to viewer",
     "app.media.label": "Media",
+    "app.meeting.ended":"This session has ended",
+    "app.meeting.endedMessage":"You will be forwarded back to the home screen",
     "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
     "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide",
     "app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
@@ -246,8 +248,6 @@
     "app.audioManager.mediaError": "Error: There was an issue getting your media devices",
     "app.audio.joinAudio": "Join Audio",
     "app.audio.leaveAudio": "Leave Audio",
-    "app.video.joinVideo": "Share Webcam",
-    "app.video.leaveVideo": "Un-share Webcam",
     "app.audio.enterSessionLabel": "Enter Session",
     "app.audio.playSoundLabel": "Play Sound",
     "app.audio.backLabel": "Back",
@@ -261,7 +261,7 @@
     "app.audio.listenOnly.closeLabel": "Close",
     "app.audio.permissionsOverlay.title": "Allow BigBlueButton to use your Media Devices",
     "app.audio.permissionsOverlay.hint": "We need you to allow us to use your Media Devices in order to join you to the voice conference :)",
-    "app.error.kicked": "You have been removed from the conference",
+    "app.error.removed": "You have been removed from the conference",
     "app.error.meeting.ended": "You have logged out of the conference",
     "app.dropdown.close": "Close",
     "app.error.500": "Ops, something went wrong",
@@ -275,6 +275,14 @@
     "app.toast.chat.plural":"you have {0} new messages in {1}",
     "app.notification.recordingStart": "This session is now being recorded",
     "app.notification.recordingStop": "This session is not being recorded anymore",
+    "app.video.joinVideo": "Share Webcam",
+    "app.video.leaveVideo": "Unshare Webcam",
+    "app.video.iceCandidateError": "Error on adding ice candidate",
+    "app.video.permissionError": "Error on sharing webcam. Please check permissions",
+    "app.video.sharingError": "Error on sharing webcam",
+    "app.video.chromeExtensionError": "You must install",
+    "app.video.chromeExtensionErrorLink": "this Chrome Extension",
+    "app.meeting.endNotification.ok.label": "OK",
     "app.video.joinVideo": "Share webcam",
     "app.video.leaveVideo": "Unshare webcam",
     "app.whiteboard.toolbar.tools": "Tools",
diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoManager.js b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
index a42a3454e80b40c8909cc7342359207d54f6fef1..7796972786f18ac247d14f751ab5a85ac235ec05 100755
--- a/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
+++ b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
@@ -41,12 +41,10 @@ var _onMessage = function (_message) {
     sessions[sessionId] = {};
   }
 
-  logAvailableSessions();
-
   switch (role) {
     case 'share':
-      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'shared']) {
-        video = sessions[sessionId][message.cameraId+'shared'];
+      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'-shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'-shared']) {
+        video = sessions[sessionId][message.cameraId+'-shared'];
       }
       break;
     case 'viewer':
@@ -54,8 +52,8 @@ var _onMessage = function (_message) {
         video = sessions[sessionId][message.cameraId];
       }
     case 'any':
-      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'shared']) {
-        video = sessions[sessionId][message.cameraId+'shared'];
+      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'-shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'-shared']) {
+        video = sessions[sessionId][message.cameraId+'-shared'];
       }
       else if (message.cameraId && sessions[sessionId][message.cameraId]) {
         video = sessions[sessionId][message.cameraId];
@@ -80,7 +78,7 @@ var _onMessage = function (_message) {
 
       switch (role) {
         case 'share':
-          sessions[sessionId][message.cameraId+'shared']= video;
+          sessions[sessionId][message.cameraId+'-shared']= video;
           break;
         case 'viewer':
           sessions[sessionId][message.cameraId] = video;
@@ -96,6 +94,7 @@ var _onMessage = function (_message) {
             role: role,
             id : 'error',
             response : 'rejected',
+            cameraId : message.cameraId,
             message : error
           }), C.FROM_VIDEO);
         }
@@ -116,7 +115,7 @@ var _onMessage = function (_message) {
       console.log('[' + message.id + '] connection ' + sessionId + " with message => " + JSON.stringify(message, null, 2));
 
       if (video) {
-        stopSession(sessionId, role, cameraId);
+        stopVideo(sessionId, role, cameraId);
       } else {
         console.log(" [stop] Why is there no video on STOP?");
       }
@@ -155,26 +154,39 @@ var _onMessage = function (_message) {
   }
 };
 
-let stopSession = async function(sessionId, role, cameraId) {
-  console.log('  [VideoManager/x] Stopping session ' + sessionId + " with role " + role + " for camera " + cameraId);
+let stopSession = async function(sessionId) {
 
   let videoIds = Object.keys(sessions[sessionId]);
 
+  for (let i=0; i < videoIds.length; i++) {
+    let camId = videoIds[i].split('-')[0], role = videoIds[i].split('-')[1];
+    await stopVideo(sessionId, role ? 'share' : 'viewer', camId);
+  }
+
+  delete sessions[sessionId];
+  logAvailableSessions();
+}
+
+let stopVideo = async function(sessionId, role, cameraId) {
+  console.log('  [VideoManager/x] Stopping session ' + sessionId + " with role " + role + " for camera " + cameraId);
+
   try {
     if (role === 'share') {
-      var sharedVideo = sessions[sessionId][cameraId+'shared'];
-      await sharedVideo.stop();
-      delete sessions[sessionId][cameraId+'shared'];
-      console.log('  [VideoManager] Stopping sharer [', sessionId, '][', cameraId,'] with IDs' , videoIds);
+      var sharedVideo = sessions[sessionId][cameraId+'-shared'];
+      if (sharedVideo) {
+        console.log('  [VideoManager] Stopping sharer [', sessionId, '][', cameraId,']');
+        await sharedVideo.stop();
+        delete sessions[sessionId][cameraId+'-shared'];
+      }
     }
     else if (role === 'viewer') {
       var video = sessions[sessionId][cameraId];
-      await video.stop();
-      delete sessions[sessionId][cameraId];
-      console.log('  [VideoManager] Stopping viewer [', sessionId, '][', cameraId,'] with IDs ', sessions[sessionId][cameraId]);
+      if (video) {
+        console.log('  [VideoManager] Stopping viewer [', sessionId, '][', cameraId,']');
+        await video.stop();
+        delete sessions[sessionId][cameraId];
+      }
     }
-
-    logAvailableSessions();
   }
   catch (err) {
     console.log("  [VideoManager] Stop error => ", err);
@@ -191,7 +203,6 @@ let stopAll = function() {
   let sessionIds = Object.keys(sessions);
 
   for (var i = 0; i < sessionIds.length; i++) {
-
     stopSession(sessionIds[i]);
   }