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?" <":" <")+dialog.params.remote_caller_id_number+(enc?">":">");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]); }