0 &&
+ obj.values.animationCountOnSteps &&
+ obj.values.animationCountOnSteps[step] <= animation_step) {
+ animation_step = obj.values.animationCountOnSteps[step];
+ }
+ // jump to animation steps by calling flyToNextStep()
+ function doAnimationSteps() {
+ if (obj.values.isMoving == true) {
+ setTimeout(doAnimationSteps, 100); // wait until the flight ends
+ return;
+ }
+ while (animation_step-- > 0) {
+ obj.flyToNextStep(); // do the animation steps
+ }
+ }
+ setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
+ // jump to the step
+ return this.sendMessage({
+ 'action': 'present',
+ 'data': ['moveToStep', step]
+ });
+ };
+
+ PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
+ PreziPlayer.prototype.flyToObject = function(objectId) {
+ return this.sendMessage({
+ 'action': 'present',
+ 'data': ['moveToObject', objectId]
+ });
+ };
+
+ PreziPlayer.prototype.play = function(defaultDelay) {
+ return this.sendMessage({
+ 'action': 'present',
+ 'data': ['startAutoPlay', defaultDelay]
+ });
+ };
+
+ PreziPlayer.prototype.stop = function() {
+ return this.sendMessage({
+ 'action': 'present',
+ 'data': ['stopAutoPlay']
+ });
+ };
+
+ PreziPlayer.prototype.pause = function(defaultDelay) {
+ return this.sendMessage({
+ 'action': 'present',
+ 'data': ['pauseAutoPlay', defaultDelay]
+ });
+ };
+
+ PreziPlayer.prototype.getCurrentStep = function() {
+ return this.values.currentStep;
+ };
+
+ PreziPlayer.prototype.getCurrentAnimationStep = function() {
+ return this.values.currentAnimationStep;
+ };
+
+ PreziPlayer.prototype.getCurrentObject = function() {
+ return this.values.currentObject;
+ };
+
+ PreziPlayer.prototype.getStatus = function() {
+ return this.values.status;
+ };
+
+ PreziPlayer.prototype.isPlaying = function() {
+ return this.values.isAutoPlaying;
+ };
+
+ PreziPlayer.prototype.getStepCount = function() {
+ return this.values.stepCount;
+ };
+
+ PreziPlayer.prototype.getAnimationCountOnSteps = function() {
+ return this.values.animationCountOnSteps;
+ };
+
+ PreziPlayer.prototype.getTitle = function() {
+ return this.values.title;
+ };
+
+ PreziPlayer.prototype.setDimensions = function(dims) {
+ for (var parameter in dims) {
+ this.iframe[parameter] = dims[parameter];
+ }
+ }
+
+ PreziPlayer.prototype.getDimensions = function() {
+ return {
+ width: parseInt(this.iframe.width, 10),
+ height: parseInt(this.iframe.height, 10)
+ }
+ }
+
+ PreziPlayer.prototype.on = function(event, callback) {
+ this.callbacks.push({
+ event: event,
+ callback: callback
+ });
+ };
+
+ PreziPlayer.prototype.off = function(event, callback) {
+ var j, item;
+ if (event === undefined) {
+ this.callbacks = [];
+ }
+ j = this.callbacks.length;
+ while (j--) {
+ item = this.callbacks[j];
+ if (item && item.event === event && (callback === undefined || item.callback === callback)){
+ this.callbacks.splice(j, 1);
+ }
+ }
+ };
+
+ if (window.addEventListener) {
+ window.addEventListener('message', PreziPlayer.messageReceived, false);
+ } else {
+ window.attachEvent('onmessage', PreziPlayer.messageReceived);
+ }
+
+ return PreziPlayer;
+
+ })();
+
+})();
+
+module.exports = PreziPlayer;
+
+},{}],18:[function(require,module,exports){
+var Chat = require("./chat/Chat");
+var ContactList = require("./contactlist/ContactList");
+var Settings = require("./../../settings/Settings");
+var SettingsMenu = require("./settings/SettingsMenu");
+var VideoLayout = require("../videolayout/VideoLayout");
+var ToolbarToggler = require("../toolbars/ToolbarToggler");
+var UIUtil = require("../util/UIUtil");
+
+/**
+ * Toggler for the chat, contact list, settings menu, etc..
+ */
+var PanelToggler = (function(my) {
+
+ var currentlyOpen = null;
+ var buttons = {
+ '#chatspace': '#chatBottomButton',
+ '#contactlist': '#contactListButton',
+ '#settingsmenu': '#settingsButton'
+ };
+
+ /**
+ * Resizes the video area
+ * @param isClosing whether the side panel is going to be closed or is going to open / remain opened
+ * @param completeFunction a function to be called when the video space is resized
+ */
+ var resizeVideoArea = function(isClosing, completeFunction) {
+ var videospace = $('#videospace');
+
+ var panelSize = isClosing ? [0, 0] : PanelToggler.getPanelSize();
+ var videospaceWidth = window.innerWidth - panelSize[0];
+ var videospaceHeight = window.innerHeight;
+ var videoSize
+ = VideoLayout.getVideoSize(null, null, videospaceWidth, videospaceHeight);
+ var videoWidth = videoSize[0];
+ var videoHeight = videoSize[1];
+ var videoPosition = VideoLayout.getVideoPosition(videoWidth,
+ videoHeight,
+ videospaceWidth,
+ videospaceHeight);
+ var horizontalIndent = videoPosition[0];
+ var verticalIndent = videoPosition[1];
+
+ var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth);
+ var thumbnailsWidth = thumbnailSize[0];
+ var thumbnailsHeight = thumbnailSize[1];
+ //for chat
+
+ videospace.animate({
+ right: panelSize[0],
+ width: videospaceWidth,
+ height: videospaceHeight
+ },
+ {
+ queue: false,
+ duration: 500,
+ complete: completeFunction
+ });
+
+ $('#remoteVideos').animate({
+ height: thumbnailsHeight
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+
+ $('#remoteVideos>span').animate({
+ height: thumbnailsHeight,
+ width: thumbnailsWidth
+ },
+ {
+ queue: false,
+ duration: 500,
+ complete: function () {
+ $(document).trigger(
+ "remotevideo.resized",
+ [thumbnailsWidth,
+ thumbnailsHeight]);
+ }
+ });
+
+ $('#largeVideoContainer').animate({
+ width: videospaceWidth,
+ height: videospaceHeight
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+
+ $('#largeVideo').animate({
+ width: videoWidth,
+ height: videoHeight,
+ top: verticalIndent,
+ bottom: verticalIndent,
+ left: horizontalIndent,
+ right: horizontalIndent
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+ };
+
+ /**
+ * Toggles the windows in the side panel
+ * @param object the window that should be shown
+ * @param selector the selector for the element containing the panel
+ * @param onOpenComplete function to be called when the panel is opened
+ * @param onOpen function to be called if the window is going to be opened
+ * @param onClose function to be called if the window is going to be closed
+ */
+ var toggle = function(object, selector, onOpenComplete, onOpen, onClose) {
+ UIUtil.buttonClick(buttons[selector], "active");
+
+ if (object.isVisible()) {
+ $("#toast-container").animate({
+ right: '5px'
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+ $(selector).hide("slide", {
+ direction: "right",
+ queue: false,
+ duration: 500
+ });
+ if(typeof onClose === "function") {
+ onClose();
+ }
+
+ currentlyOpen = null;
+ }
+ else {
+ // Undock the toolbar when the chat is shown and if we're in a
+ // video mode.
+ if (VideoLayout.isLargeVideoVisible()) {
+ ToolbarToggler.dockToolbar(false);
+ }
+
+ if(currentlyOpen) {
+ var current = $(currentlyOpen);
+ UIUtil.buttonClick(buttons[currentlyOpen], "active");
+ current.css('z-index', 4);
+ setTimeout(function () {
+ current.css('display', 'none');
+ current.css('z-index', 5);
+ }, 500);
+ }
+
+ $("#toast-container").animate({
+ right: (PanelToggler.getPanelSize()[0] + 5) + 'px'
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+ $(selector).show("slide", {
+ direction: "right",
+ queue: false,
+ duration: 500,
+ complete: onOpenComplete
+ });
+ if(typeof onOpen === "function") {
+ onOpen();
+ }
+
+ currentlyOpen = selector;
+ }
+ };
+
+ /**
+ * Opens / closes the chat area.
+ */
+ my.toggleChat = function() {
+ var chatCompleteFunction = Chat.isVisible() ?
+ function() {} : function () {
+ Chat.scrollChatToBottom();
+ $('#chatspace').trigger('shown');
+ };
+
+ resizeVideoArea(Chat.isVisible(), chatCompleteFunction);
+
+ toggle(Chat,
+ '#chatspace',
+ function () {
+ // Request the focus in the nickname field or the chat input field.
+ if ($('#nickname').css('visibility') === 'visible') {
+ $('#nickinput').focus();
+ } else {
+ $('#usermsg').focus();
+ }
+ },
+ null,
+ Chat.resizeChat,
+ null);
+ };
+
+ /**
+ * Opens / closes the contact list area.
+ */
+ my.toggleContactList = function () {
+ var completeFunction = ContactList.isVisible() ?
+ function() {} : function () { $('#contactlist').trigger('shown');};
+ resizeVideoArea(ContactList.isVisible(), completeFunction);
+
+ toggle(ContactList,
+ '#contactlist',
+ null,
+ function() {
+ ContactList.setVisualNotification(false);
+ },
+ null);
+ };
+
+ /**
+ * Opens / closes the settings menu
+ */
+ my.toggleSettingsMenu = function() {
+ resizeVideoArea(SettingsMenu.isVisible(), function (){});
+ toggle(SettingsMenu,
+ '#settingsmenu',
+ null,
+ function() {
+ var settings = Settings.getSettings();
+ $('#setDisplayName').get(0).value = settings.displayName;
+ $('#setEmail').get(0).value = settings.email;
+ },
+ null);
+ };
+
+ /**
+ * Returns the size of the side panel.
+ */
+ my.getPanelSize = function () {
+ var availableHeight = window.innerHeight;
+ var availableWidth = window.innerWidth;
+
+ var panelWidth = 200;
+ if (availableWidth * 0.2 < 200) {
+ panelWidth = availableWidth * 0.2;
+ }
+
+ return [panelWidth, availableHeight];
+ };
+
+ my.isVisible = function() {
+ return (Chat.isVisible() || ContactList.isVisible() || SettingsMenu.isVisible());
+ };
+
+ return my;
+
+}(PanelToggler || {}));
+
module.exports = PanelToggler;
-},{"../toolbars/ToolbarToggler":26,"../util/UIUtil":30,"../videolayout/VideoLayout":32,"./../../settings/Settings":38,"./chat/Chat":18,"./contactlist/ContactList":22,"./settings/SettingsMenu":23}],18:[function(require,module,exports){
-/* global $, Util, nickname:true */
-var Replacement = require("./Replacement");
-var CommandsProcessor = require("./Commands");
-var ToolbarToggler = require("../../toolbars/ToolbarToggler");
-var smileys = require("./smileys.json").smileys;
-var NicknameHandler = require("../../util/NicknameHandler");
-var UIUtil = require("../../util/UIUtil");
-var UIEvents = require("../../../../service/UI/UIEvents");
-
-var notificationInterval = false;
-var unreadMessages = 0;
-
-
-/**
- * Shows/hides a visual notification, indicating that a message has arrived.
- */
-function setVisualNotification(show) {
- var unreadMsgElement = document.getElementById('unreadMessages');
- var unreadMsgBottomElement
- = document.getElementById('bottomUnreadMessages');
-
- var glower = $('#chatButton');
- var bottomGlower = $('#chatBottomButton');
-
- if (unreadMessages) {
- unreadMsgElement.innerHTML = unreadMessages.toString();
- unreadMsgBottomElement.innerHTML = unreadMessages.toString();
-
- ToolbarToggler.dockToolbar(true);
-
- var chatButtonElement
- = document.getElementById('chatButton').parentNode;
- var leftIndent = (UIUtil.getTextWidth(chatButtonElement) -
- UIUtil.getTextWidth(unreadMsgElement)) / 2;
- var topIndent = (UIUtil.getTextHeight(chatButtonElement) -
- UIUtil.getTextHeight(unreadMsgElement)) / 2 - 3;
-
- unreadMsgElement.setAttribute(
- 'style',
- 'top:' + topIndent +
- '; left:' + leftIndent + ';');
-
- var chatBottomButtonElement
- = document.getElementById('chatBottomButton').parentNode;
- var bottomLeftIndent = (UIUtil.getTextWidth(chatBottomButtonElement) -
- UIUtil.getTextWidth(unreadMsgBottomElement)) / 2;
- var bottomTopIndent = (UIUtil.getTextHeight(chatBottomButtonElement) -
- UIUtil.getTextHeight(unreadMsgBottomElement)) / 2 - 2;
-
- unreadMsgBottomElement.setAttribute(
- 'style',
- 'top:' + bottomTopIndent +
- '; left:' + bottomLeftIndent + ';');
-
-
- if (!glower.hasClass('icon-chat-simple')) {
- glower.removeClass('icon-chat');
- glower.addClass('icon-chat-simple');
- }
- }
- else {
- unreadMsgElement.innerHTML = '';
- unreadMsgBottomElement.innerHTML = '';
- glower.removeClass('icon-chat-simple');
- glower.addClass('icon-chat');
- }
-
- if (show && !notificationInterval) {
- notificationInterval = window.setInterval(function () {
- glower.toggleClass('active');
- bottomGlower.toggleClass('active glowing');
- }, 800);
- }
- else if (!show && notificationInterval) {
- window.clearInterval(notificationInterval);
- notificationInterval = false;
- glower.removeClass('active');
- bottomGlower.removeClass('glowing');
- bottomGlower.addClass('active');
- }
-}
-
-
-/**
- * Returns the current time in the format it is shown to the user
- * @returns {string}
- */
-function getCurrentTime() {
- var now = new Date();
- var hour = now.getHours();
- var minute = now.getMinutes();
- var second = now.getSeconds();
- if(hour.toString().length === 1) {
- hour = '0'+hour;
- }
- if(minute.toString().length === 1) {
- minute = '0'+minute;
- }
- if(second.toString().length === 1) {
- second = '0'+second;
- }
- return hour+':'+minute+':'+second;
-}
-
-function toggleSmileys()
-{
- var smileys = $('#smileysContainer');
- if(!smileys.is(':visible')) {
- smileys.show("slide", { direction: "down", duration: 300});
- } else {
- smileys.hide("slide", { direction: "down", duration: 300});
- }
- $('#usermsg').focus();
-}
-
-function addClickFunction(smiley, number) {
- smiley.onclick = function addSmileyToMessage() {
- var usermsg = $('#usermsg');
- var message = usermsg.val();
- message += smileys['smiley' + number];
- usermsg.val(message);
- usermsg.get(0).setSelectionRange(message.length, message.length);
- toggleSmileys();
- usermsg.focus();
- };
-}
-
-/**
- * Adds the smileys container to the chat
- */
-function addSmileys() {
- var smileysContainer = document.createElement('div');
- smileysContainer.id = 'smileysContainer';
- for(var i = 1; i <= 21; i++) {
- var smileyContainer = document.createElement('div');
- smileyContainer.id = 'smiley' + i;
- smileyContainer.className = 'smileyContainer';
- var smiley = document.createElement('img');
- smiley.src = 'images/smileys/smiley' + i + '.svg';
- smiley.className = 'smiley';
- addClickFunction(smiley, i);
- smileyContainer.appendChild(smiley);
- smileysContainer.appendChild(smileyContainer);
- }
-
- $("#chatspace").append(smileysContainer);
-}
-
-/**
- * Resizes the chat conversation.
- */
-function resizeChatConversation() {
- var msgareaHeight = $('#usermsg').outerHeight();
- var chatspace = $('#chatspace');
- var width = chatspace.width();
- var chat = $('#chatconversation');
- var smileys = $('#smileysarea');
-
- smileys.height(msgareaHeight);
- $("#smileys").css('bottom', (msgareaHeight - 26) / 2);
- $('#smileysContainer').css('bottom', msgareaHeight);
- chat.width(width - 10);
- chat.height(window.innerHeight - 15 - msgareaHeight);
-}
-
-/**
- * Chat related user interface.
- */
-var Chat = (function (my) {
- /**
- * Initializes chat related interface.
- */
- my.init = function () {
- if(NicknameHandler.getNickname())
- Chat.setChatConversationMode(true);
- NicknameHandler.addListener(UIEvents.NICKNAME_CHANGED,
- function (nickname) {
- Chat.setChatConversationMode(true);
- });
-
- $('#nickinput').keydown(function (event) {
- if (event.keyCode === 13) {
- event.preventDefault();
- var val = UIUtil.escapeHtml(this.value);
- this.value = '';
- if (!NicknameHandler.getNickname()) {
- NicknameHandler.setNickname(val);
-
- return;
- }
- }
- });
-
- $('#usermsg').keydown(function (event) {
- if (event.keyCode === 13) {
- event.preventDefault();
- var value = this.value;
- $('#usermsg').val('').trigger('autosize.resize');
- this.focus();
- var command = new CommandsProcessor(value);
- if(command.isCommand())
- {
- command.processCommand();
- }
- else
- {
- var message = UIUtil.escapeHtml(value);
- APP.xmpp.sendChatMessage(message, NicknameHandler.getNickname());
- }
- }
- });
-
- var onTextAreaResize = function () {
- resizeChatConversation();
- Chat.scrollChatToBottom();
- };
- $('#usermsg').autosize({callback: onTextAreaResize});
-
- $("#chatspace").bind("shown",
- function () {
- unreadMessages = 0;
- setVisualNotification(false);
- });
-
- addSmileys();
- };
-
- /**
- * Appends the given message to the chat conversation.
- */
- my.updateChatConversation = function (from, displayName, message) {
- var divClassName = '';
-
- if (APP.xmpp.myJid() === from) {
- divClassName = "localuser";
- }
- else {
- divClassName = "remoteuser";
-
- if (!Chat.isVisible()) {
- unreadMessages++;
- UIUtil.playSoundNotification('chatNotification');
- setVisualNotification(true);
- }
- }
-
- // replace links and smileys
- // Strophe already escapes special symbols on sending,
- // so we escape here only tags to avoid double &
- var escMessage = message.replace(//g, '>').replace(/\n/g, ' ');
- var escDisplayName = UIUtil.escapeHtml(displayName);
- message = Replacement.processReplacements(escMessage);
-
- var messageContainer =
- ''+
- '
' +
- '
' + escDisplayName +
- '
' + '
' + getCurrentTime() +
- '
' + '
' + message + '
' +
- '
';
-
- $('#chatconversation').append(messageContainer);
- $('#chatconversation').animate(
- { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
- };
-
- /**
- * Appends error message to the conversation
- * @param errorMessage the received error message.
- * @param originalText the original message.
- */
- my.chatAddError = function(errorMessage, originalText)
- {
- errorMessage = UIUtil.escapeHtml(errorMessage);
- originalText = UIUtil.escapeHtml(originalText);
-
- $('#chatconversation').append(
- 'Error: ' + 'Your message' +
- (originalText? (' \"'+ originalText + '\"') : "") +
- ' was not sent.' +
- (errorMessage? (' Reason: ' + errorMessage) : '') + '
');
- $('#chatconversation').animate(
- { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
- };
-
- /**
- * Sets the subject to the UI
- * @param subject the subject
- */
- my.chatSetSubject = function(subject)
- {
- if(subject)
- subject = subject.trim();
- $('#subject').html(Replacement.linkify(UIUtil.escapeHtml(subject)));
- if(subject === "")
- {
- $("#subject").css({display: "none"});
- }
- else
- {
- $("#subject").css({display: "block"});
- }
- };
-
-
-
- /**
- * Sets the chat conversation mode.
- */
- my.setChatConversationMode = function (isConversationMode) {
- if (isConversationMode) {
- $('#nickname').css({visibility: 'hidden'});
- $('#chatconversation').css({visibility: 'visible'});
- $('#usermsg').css({visibility: 'visible'});
- $('#smileysarea').css({visibility: 'visible'});
- $('#usermsg').focus();
- }
- };
-
- /**
- * Resizes the chat area.
- */
- my.resizeChat = function () {
- var chatSize = require("../SidePanelToggler").getPanelSize();
-
- $('#chatspace').width(chatSize[0]);
- $('#chatspace').height(chatSize[1]);
-
- resizeChatConversation();
- };
-
- /**
- * Indicates if the chat is currently visible.
- */
- my.isVisible = function () {
- return $('#chatspace').is(":visible");
- };
- /**
- * Shows and hides the window with the smileys
- */
- my.toggleSmileys = toggleSmileys;
-
- /**
- * Scrolls chat to the bottom.
- */
- my.scrollChatToBottom = function() {
- setTimeout(function () {
- $('#chatconversation').scrollTop(
- $('#chatconversation')[0].scrollHeight);
- }, 5);
- };
-
-
- return my;
-}(Chat || {}));
+},{"../toolbars/ToolbarToggler":27,"../util/UIUtil":31,"../videolayout/VideoLayout":33,"./../../settings/Settings":39,"./chat/Chat":19,"./contactlist/ContactList":23,"./settings/SettingsMenu":24}],19:[function(require,module,exports){
+/* global $, Util, nickname:true */
+var Replacement = require("./Replacement");
+var CommandsProcessor = require("./Commands");
+var ToolbarToggler = require("../../toolbars/ToolbarToggler");
+var smileys = require("./smileys.json").smileys;
+var NicknameHandler = require("../../util/NicknameHandler");
+var UIUtil = require("../../util/UIUtil");
+var UIEvents = require("../../../../service/UI/UIEvents");
+
+var notificationInterval = false;
+var unreadMessages = 0;
+
+
+/**
+ * Shows/hides a visual notification, indicating that a message has arrived.
+ */
+function setVisualNotification(show) {
+ var unreadMsgElement = document.getElementById('unreadMessages');
+ var unreadMsgBottomElement
+ = document.getElementById('bottomUnreadMessages');
+
+ var glower = $('#chatButton');
+ var bottomGlower = $('#chatBottomButton');
+
+ if (unreadMessages) {
+ unreadMsgElement.innerHTML = unreadMessages.toString();
+ unreadMsgBottomElement.innerHTML = unreadMessages.toString();
+
+ ToolbarToggler.dockToolbar(true);
+
+ var chatButtonElement
+ = document.getElementById('chatButton').parentNode;
+ var leftIndent = (UIUtil.getTextWidth(chatButtonElement) -
+ UIUtil.getTextWidth(unreadMsgElement)) / 2;
+ var topIndent = (UIUtil.getTextHeight(chatButtonElement) -
+ UIUtil.getTextHeight(unreadMsgElement)) / 2 - 3;
+
+ unreadMsgElement.setAttribute(
+ 'style',
+ 'top:' + topIndent +
+ '; left:' + leftIndent + ';');
+
+ var chatBottomButtonElement
+ = document.getElementById('chatBottomButton').parentNode;
+ var bottomLeftIndent = (UIUtil.getTextWidth(chatBottomButtonElement) -
+ UIUtil.getTextWidth(unreadMsgBottomElement)) / 2;
+ var bottomTopIndent = (UIUtil.getTextHeight(chatBottomButtonElement) -
+ UIUtil.getTextHeight(unreadMsgBottomElement)) / 2 - 2;
+
+ unreadMsgBottomElement.setAttribute(
+ 'style',
+ 'top:' + bottomTopIndent +
+ '; left:' + bottomLeftIndent + ';');
+
+
+ if (!glower.hasClass('icon-chat-simple')) {
+ glower.removeClass('icon-chat');
+ glower.addClass('icon-chat-simple');
+ }
+ }
+ else {
+ unreadMsgElement.innerHTML = '';
+ unreadMsgBottomElement.innerHTML = '';
+ glower.removeClass('icon-chat-simple');
+ glower.addClass('icon-chat');
+ }
+
+ if (show && !notificationInterval) {
+ notificationInterval = window.setInterval(function () {
+ glower.toggleClass('active');
+ bottomGlower.toggleClass('active glowing');
+ }, 800);
+ }
+ else if (!show && notificationInterval) {
+ window.clearInterval(notificationInterval);
+ notificationInterval = false;
+ glower.removeClass('active');
+ bottomGlower.removeClass('glowing');
+ bottomGlower.addClass('active');
+ }
+}
+
+
+/**
+ * Returns the current time in the format it is shown to the user
+ * @returns {string}
+ */
+function getCurrentTime() {
+ var now = new Date();
+ var hour = now.getHours();
+ var minute = now.getMinutes();
+ var second = now.getSeconds();
+ if(hour.toString().length === 1) {
+ hour = '0'+hour;
+ }
+ if(minute.toString().length === 1) {
+ minute = '0'+minute;
+ }
+ if(second.toString().length === 1) {
+ second = '0'+second;
+ }
+ return hour+':'+minute+':'+second;
+}
+
+function toggleSmileys()
+{
+ var smileys = $('#smileysContainer');
+ if(!smileys.is(':visible')) {
+ smileys.show("slide", { direction: "down", duration: 300});
+ } else {
+ smileys.hide("slide", { direction: "down", duration: 300});
+ }
+ $('#usermsg').focus();
+}
+
+function addClickFunction(smiley, number) {
+ smiley.onclick = function addSmileyToMessage() {
+ var usermsg = $('#usermsg');
+ var message = usermsg.val();
+ message += smileys['smiley' + number];
+ usermsg.val(message);
+ usermsg.get(0).setSelectionRange(message.length, message.length);
+ toggleSmileys();
+ usermsg.focus();
+ };
+}
+
+/**
+ * Adds the smileys container to the chat
+ */
+function addSmileys() {
+ var smileysContainer = document.createElement('div');
+ smileysContainer.id = 'smileysContainer';
+ for(var i = 1; i <= 21; i++) {
+ var smileyContainer = document.createElement('div');
+ smileyContainer.id = 'smiley' + i;
+ smileyContainer.className = 'smileyContainer';
+ var smiley = document.createElement('img');
+ smiley.src = 'images/smileys/smiley' + i + '.svg';
+ smiley.className = 'smiley';
+ addClickFunction(smiley, i);
+ smileyContainer.appendChild(smiley);
+ smileysContainer.appendChild(smileyContainer);
+ }
+
+ $("#chatspace").append(smileysContainer);
+}
+
+/**
+ * Resizes the chat conversation.
+ */
+function resizeChatConversation() {
+ var msgareaHeight = $('#usermsg').outerHeight();
+ var chatspace = $('#chatspace');
+ var width = chatspace.width();
+ var chat = $('#chatconversation');
+ var smileys = $('#smileysarea');
+
+ smileys.height(msgareaHeight);
+ $("#smileys").css('bottom', (msgareaHeight - 26) / 2);
+ $('#smileysContainer').css('bottom', msgareaHeight);
+ chat.width(width - 10);
+ chat.height(window.innerHeight - 15 - msgareaHeight);
+}
+
+/**
+ * Chat related user interface.
+ */
+var Chat = (function (my) {
+ /**
+ * Initializes chat related interface.
+ */
+ my.init = function () {
+ if(NicknameHandler.getNickname())
+ Chat.setChatConversationMode(true);
+ NicknameHandler.addListener(UIEvents.NICKNAME_CHANGED,
+ function (nickname) {
+ Chat.setChatConversationMode(true);
+ });
+
+ $('#nickinput').keydown(function (event) {
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ var val = UIUtil.escapeHtml(this.value);
+ this.value = '';
+ if (!NicknameHandler.getNickname()) {
+ NicknameHandler.setNickname(val);
+
+ return;
+ }
+ }
+ });
+
+ $('#usermsg').keydown(function (event) {
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ var value = this.value;
+ $('#usermsg').val('').trigger('autosize.resize');
+ this.focus();
+ var command = new CommandsProcessor(value);
+ if(command.isCommand())
+ {
+ command.processCommand();
+ }
+ else
+ {
+ var message = UIUtil.escapeHtml(value);
+ APP.xmpp.sendChatMessage(message, NicknameHandler.getNickname());
+ }
+ }
+ });
+
+ var onTextAreaResize = function () {
+ resizeChatConversation();
+ Chat.scrollChatToBottom();
+ };
+ $('#usermsg').autosize({callback: onTextAreaResize});
+
+ $("#chatspace").bind("shown",
+ function () {
+ unreadMessages = 0;
+ setVisualNotification(false);
+ });
+
+ addSmileys();
+ };
+
+ /**
+ * Appends the given message to the chat conversation.
+ */
+ my.updateChatConversation = function (from, displayName, message) {
+ var divClassName = '';
+
+ if (APP.xmpp.myJid() === from) {
+ divClassName = "localuser";
+ }
+ else {
+ divClassName = "remoteuser";
+
+ if (!Chat.isVisible()) {
+ unreadMessages++;
+ UIUtil.playSoundNotification('chatNotification');
+ setVisualNotification(true);
+ }
+ }
+
+ // replace links and smileys
+ // Strophe already escapes special symbols on sending,
+ // so we escape here only tags to avoid double &
+ var escMessage = message.replace(//g, '>').replace(/\n/g, ' ');
+ var escDisplayName = UIUtil.escapeHtml(displayName);
+ message = Replacement.processReplacements(escMessage);
+
+ var messageContainer =
+ ''+
+ '
' +
+ '
' + escDisplayName +
+ '
' + '
' + getCurrentTime() +
+ '
' + '
' + message + '
' +
+ '
';
+
+ $('#chatconversation').append(messageContainer);
+ $('#chatconversation').animate(
+ { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
+ };
+
+ /**
+ * Appends error message to the conversation
+ * @param errorMessage the received error message.
+ * @param originalText the original message.
+ */
+ my.chatAddError = function(errorMessage, originalText)
+ {
+ errorMessage = UIUtil.escapeHtml(errorMessage);
+ originalText = UIUtil.escapeHtml(originalText);
+
+ $('#chatconversation').append(
+ 'Error: ' + 'Your message' +
+ (originalText? (' \"'+ originalText + '\"') : "") +
+ ' was not sent.' +
+ (errorMessage? (' Reason: ' + errorMessage) : '') + '
');
+ $('#chatconversation').animate(
+ { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
+ };
+
+ /**
+ * Sets the subject to the UI
+ * @param subject the subject
+ */
+ my.chatSetSubject = function(subject)
+ {
+ if(subject)
+ subject = subject.trim();
+ $('#subject').html(Replacement.linkify(UIUtil.escapeHtml(subject)));
+ if(subject === "")
+ {
+ $("#subject").css({display: "none"});
+ }
+ else
+ {
+ $("#subject").css({display: "block"});
+ }
+ };
+
+
+
+ /**
+ * Sets the chat conversation mode.
+ */
+ my.setChatConversationMode = function (isConversationMode) {
+ if (isConversationMode) {
+ $('#nickname').css({visibility: 'hidden'});
+ $('#chatconversation').css({visibility: 'visible'});
+ $('#usermsg').css({visibility: 'visible'});
+ $('#smileysarea').css({visibility: 'visible'});
+ $('#usermsg').focus();
+ }
+ };
+
+ /**
+ * Resizes the chat area.
+ */
+ my.resizeChat = function () {
+ var chatSize = require("../SidePanelToggler").getPanelSize();
+
+ $('#chatspace').width(chatSize[0]);
+ $('#chatspace').height(chatSize[1]);
+
+ resizeChatConversation();
+ };
+
+ /**
+ * Indicates if the chat is currently visible.
+ */
+ my.isVisible = function () {
+ return $('#chatspace').is(":visible");
+ };
+ /**
+ * Shows and hides the window with the smileys
+ */
+ my.toggleSmileys = toggleSmileys;
+
+ /**
+ * Scrolls chat to the bottom.
+ */
+ my.scrollChatToBottom = function() {
+ setTimeout(function () {
+ $('#chatconversation').scrollTop(
+ $('#chatconversation')[0].scrollHeight);
+ }, 5);
+ };
+
+
+ return my;
+}(Chat || {}));
module.exports = Chat;
-},{"../../../../service/UI/UIEvents":92,"../../toolbars/ToolbarToggler":26,"../../util/NicknameHandler":29,"../../util/UIUtil":30,"../SidePanelToggler":17,"./Commands":19,"./Replacement":20,"./smileys.json":21}],19:[function(require,module,exports){
-var UIUtil = require("../../util/UIUtil");
-
-/**
- * List with supported commands. The keys are the names of the commands and
- * the value is the function that processes the message.
- * @type {{String: function}}
- */
-var commands = {
- "topic" : processTopic
-};
-
-/**
- * Extracts the command from the message.
- * @param message the received message
- * @returns {string} the command
- */
-function getCommand(message)
-{
- if(message)
- {
- for(var command in commands)
- {
- if(message.indexOf("/" + command) == 0)
- return command;
- }
- }
- return "";
-};
-
-/**
- * Processes the data for topic command.
- * @param commandArguments the arguments of the topic command.
- */
-function processTopic(commandArguments)
-{
- var topic = UIUtil.escapeHtml(commandArguments);
- APP.xmpp.setSubject(topic);
-}
-
-/**
- * Constructs new CommandProccessor instance from a message that
- * handles commands received via chat messages.
- * @param message the message
- * @constructor
- */
-function CommandsProcessor(message)
-{
-
-
- var command = getCommand(message);
-
- /**
- * Returns the name of the command.
- * @returns {String} the command
- */
- this.getCommand = function()
- {
- return command;
- };
-
-
- var messageArgument = message.substr(command.length + 2);
-
- /**
- * Returns the arguments of the command.
- * @returns {string}
- */
- this.getArgument = function()
- {
- return messageArgument;
- };
-}
-
-/**
- * Checks whether this instance is valid command or not.
- * @returns {boolean}
- */
-CommandsProcessor.prototype.isCommand = function()
-{
- if(this.getCommand())
- return true;
- return false;
-};
-
-/**
- * Processes the command.
- */
-CommandsProcessor.prototype.processCommand = function()
-{
- if(!this.isCommand())
- return;
-
- commands[this.getCommand()](this.getArgument());
-
-};
-
+},{"../../../../service/UI/UIEvents":93,"../../toolbars/ToolbarToggler":27,"../../util/NicknameHandler":30,"../../util/UIUtil":31,"../SidePanelToggler":18,"./Commands":20,"./Replacement":21,"./smileys.json":22}],20:[function(require,module,exports){
+var UIUtil = require("../../util/UIUtil");
+
+/**
+ * List with supported commands. The keys are the names of the commands and
+ * the value is the function that processes the message.
+ * @type {{String: function}}
+ */
+var commands = {
+ "topic" : processTopic
+};
+
+/**
+ * Extracts the command from the message.
+ * @param message the received message
+ * @returns {string} the command
+ */
+function getCommand(message)
+{
+ if(message)
+ {
+ for(var command in commands)
+ {
+ if(message.indexOf("/" + command) == 0)
+ return command;
+ }
+ }
+ return "";
+};
+
+/**
+ * Processes the data for topic command.
+ * @param commandArguments the arguments of the topic command.
+ */
+function processTopic(commandArguments)
+{
+ var topic = UIUtil.escapeHtml(commandArguments);
+ APP.xmpp.setSubject(topic);
+}
+
+/**
+ * Constructs new CommandProccessor instance from a message that
+ * handles commands received via chat messages.
+ * @param message the message
+ * @constructor
+ */
+function CommandsProcessor(message)
+{
+
+
+ var command = getCommand(message);
+
+ /**
+ * Returns the name of the command.
+ * @returns {String} the command
+ */
+ this.getCommand = function()
+ {
+ return command;
+ };
+
+
+ var messageArgument = message.substr(command.length + 2);
+
+ /**
+ * Returns the arguments of the command.
+ * @returns {string}
+ */
+ this.getArgument = function()
+ {
+ return messageArgument;
+ };
+}
+
+/**
+ * Checks whether this instance is valid command or not.
+ * @returns {boolean}
+ */
+CommandsProcessor.prototype.isCommand = function()
+{
+ if(this.getCommand())
+ return true;
+ return false;
+};
+
+/**
+ * Processes the command.
+ */
+CommandsProcessor.prototype.processCommand = function()
+{
+ if(!this.isCommand())
+ return;
+
+ commands[this.getCommand()](this.getArgument());
+
+};
+
module.exports = CommandsProcessor;
-},{"../../util/UIUtil":30}],20:[function(require,module,exports){
-var Smileys = require("./smileys.json");
-/**
- * Processes links and smileys in "body"
- */
-function processReplacements(body)
-{
- //make links clickable
- body = linkify(body);
+},{"../../util/UIUtil":31}],21:[function(require,module,exports){
+var Smileys = require("./smileys.json");
+/**
+ * Processes links and smileys in "body"
+ */
+function processReplacements(body)
+{
+ //make links clickable
+ body = linkify(body);
+
+ //add smileys
+ body = smilify(body);
+
+ return body;
+}
+
+/**
+ * Finds and replaces all links in the links in "body"
+ * with their
+ */
+function linkify(inputText)
+{
+ var replacedText, replacePattern1, replacePattern2, replacePattern3;
+
+ //URLs starting with http://, https://, or ftp://
+ replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
+ replacedText = inputText.replace(replacePattern1, '$1 ');
+
+ //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
+ replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
+ replacedText = replacedText.replace(replacePattern2, '$1$2 ');
+
+ //Change email addresses to mailto:: links.
+ replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
+ replacedText = replacedText.replace(replacePattern3, '$1 ');
+
+ return replacedText;
+}
+
+/**
+ * Replaces common smiley strings with images
+ */
+function smilify(body)
+{
+ if(!body) {
+ return body;
+ }
+
+ var regexs = Smileys["regexs"];
+ for(var smiley in regexs) {
+ if(regexs.hasOwnProperty(smiley)) {
+ body = body.replace(regexs[smiley],
+ ' ');
+ }
+ }
+
+ return body;
+}
+
+module.exports = {
+ processReplacements: processReplacements,
+ linkify: linkify
+};
- //add smileys
- body = smilify(body);
+},{"./smileys.json":22}],22:[function(require,module,exports){
+module.exports={
+ "smileys": {
+ "smiley1": ":)",
+ "smiley2": ":(",
+ "smiley3": ":D",
+ "smiley4": "(y)",
+ "smiley5": " :P",
+ "smiley6": "(wave)",
+ "smiley7": "(blush)",
+ "smiley8": "(chuckle)",
+ "smiley9": "(shocked)",
+ "smiley10": ":*",
+ "smiley11": "(n)",
+ "smiley12": "(search)",
+ "smiley13": " <3",
+ "smiley14": "(oops)",
+ "smiley15": "(angry)",
+ "smiley16": "(angel)",
+ "smiley17": "(sick)",
+ "smiley18": ";(",
+ "smiley19": "(bomb)",
+ "smiley20": "(clap)",
+ "smiley21": " ;)"
+ },
+ "regexs": {
+ "smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
+ "smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
+ "smiley1": /(:-\)|:\))/gi,
+ "smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
+ "smiley5": /(:-P|:P|:-p|:p)/gi,
+ "smiley6": /(\(wave\))/gi,
+ "smiley7": /(\(blush\))/gi,
+ "smiley8": /(\(chuckle\))/gi,
+ "smiley9": /(:-0|\(shocked\))/gi,
+ "smiley10": /(:-\*|:\*|\(kiss\))/gi,
+ "smiley11": /(\(n\))/gi,
+ "smiley12": /(\(search\))/g,
+ "smiley13": /(<3|<3|<3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
+ "smiley14": /(\(oops\))/gi,
+ "smiley15": /(\(angry\))/gi,
+ "smiley16": /(\(angel\))/gi,
+ "smiley17": /(\(sick\))/gi,
+ "smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
+ "smiley19": /(\(bomb\))/gi,
+ "smiley20": /(\(clap\))/gi,
+ "smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
+ }
+}
- return body;
-}
-
-/**
- * Finds and replaces all links in the links in "body"
- * with their
- */
-function linkify(inputText)
-{
- var replacedText, replacePattern1, replacePattern2, replacePattern3;
-
- //URLs starting with http://, https://, or ftp://
- replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
- replacedText = inputText.replace(replacePattern1, '$1 ');
-
- //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
- replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
- replacedText = replacedText.replace(replacePattern2, '$1$2 ');
-
- //Change email addresses to mailto:: links.
- replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
- replacedText = replacedText.replace(replacePattern3, '$1 ');
-
- return replacedText;
-}
-
-/**
- * Replaces common smiley strings with images
- */
-function smilify(body)
-{
- if(!body) {
- return body;
- }
-
- var regexs = Smileys["regexs"];
- for(var smiley in regexs) {
- if(regexs.hasOwnProperty(smiley)) {
- body = body.replace(regexs[smiley],
- ' ');
- }
- }
-
- return body;
-}
-
-module.exports = {
- processReplacements: processReplacements,
- linkify: linkify
-};
-
-},{"./smileys.json":21}],21:[function(require,module,exports){
-module.exports={
- "smileys": {
- "smiley1": ":)",
- "smiley2": ":(",
- "smiley3": ":D",
- "smiley4": "(y)",
- "smiley5": " :P",
- "smiley6": "(wave)",
- "smiley7": "(blush)",
- "smiley8": "(chuckle)",
- "smiley9": "(shocked)",
- "smiley10": ":*",
- "smiley11": "(n)",
- "smiley12": "(search)",
- "smiley13": " <3",
- "smiley14": "(oops)",
- "smiley15": "(angry)",
- "smiley16": "(angel)",
- "smiley17": "(sick)",
- "smiley18": ";(",
- "smiley19": "(bomb)",
- "smiley20": "(clap)",
- "smiley21": " ;)"
- },
- "regexs": {
- "smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
- "smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
- "smiley1": /(:-\)|:\))/gi,
- "smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
- "smiley5": /(:-P|:P|:-p|:p)/gi,
- "smiley6": /(\(wave\))/gi,
- "smiley7": /(\(blush\))/gi,
- "smiley8": /(\(chuckle\))/gi,
- "smiley9": /(:-0|\(shocked\))/gi,
- "smiley10": /(:-\*|:\*|\(kiss\))/gi,
- "smiley11": /(\(n\))/gi,
- "smiley12": /(\(search\))/g,
- "smiley13": /(<3|<3|<3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
- "smiley14": /(\(oops\))/gi,
- "smiley15": /(\(angry\))/gi,
- "smiley16": /(\(angel\))/gi,
- "smiley17": /(\(sick\))/gi,
- "smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
- "smiley19": /(\(bomb\))/gi,
- "smiley20": /(\(clap\))/gi,
- "smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
- }
-}
-
-},{}],22:[function(require,module,exports){
-
-var numberOfContacts = 0;
-var notificationInterval;
-
-/**
- * Updates the number of participants in the contact list button and sets
- * the glow
- * @param delta indicates whether a new user has joined (1) or someone has
- * left(-1)
- */
-function updateNumberOfParticipants(delta) {
- //when the user is alone we don't show the number of participants
- if(numberOfContacts === 0) {
- $("#numberOfParticipants").text('');
- numberOfContacts += delta;
- } else if(numberOfContacts !== 0 && !ContactList.isVisible()) {
- ContactList.setVisualNotification(true);
- numberOfContacts += delta;
- $("#numberOfParticipants").text(numberOfContacts);
- }
-}
-
-/**
- * Creates the avatar element.
- *
- * @return the newly created avatar element
- */
-function createAvatar(id) {
- var avatar = document.createElement('img');
- avatar.className = "icon-avatar avatar";
- avatar.src = "https://www.gravatar.com/avatar/" + id + "?d=wavatar&size=30";
-
- return avatar;
-}
-
-/**
- * Creates the display name paragraph.
- *
- * @param displayName the display name to set
- */
-function createDisplayNameParagraph(key, displayName) {
- var p = document.createElement('p');
- if(displayName)
- p.innerText = displayName;
- else if(key)
- {
- p.setAttribute("data-i18n",key);
- p.innerText = APP.translation.translateString(key);
- }
-
- return p;
-}
-
-
-function stopGlowing(glower) {
- window.clearInterval(notificationInterval);
- notificationInterval = false;
- glower.removeClass('glowing');
- if (!ContactList.isVisible()) {
- glower.removeClass('active');
- }
-}
-
-
-/**
- * Contact list.
- */
-var ContactList = {
- /**
- * Indicates if the chat is currently visible.
- *
- * @return true if the chat is currently visible, false -
- * otherwise
- */
- isVisible: function () {
- return $('#contactlist').is(":visible");
- },
-
- /**
- * Adds a contact for the given peerJid if such doesn't yet exist.
- *
- * @param peerJid the peerJid corresponding to the contact
- * @param id the user's email or userId used to get the user's avatar
- */
- ensureAddContact: function (peerJid, id) {
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
-
- if (!contact || contact.length <= 0)
- ContactList.addContact(peerJid, id);
- },
-
- /**
- * Adds a contact for the given peer jid.
- *
- * @param peerJid the jid of the contact to add
- * @param id the email or userId of the user
- */
- addContact: function (peerJid, id) {
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contactlist = $('#contactlist>ul');
-
- var newContact = document.createElement('li');
- newContact.id = resourceJid;
- newContact.className = "clickable";
- newContact.onclick = function (event) {
- if (event.currentTarget.className === "clickable") {
- $(ContactList).trigger('contactclicked', [peerJid]);
- }
- };
-
- newContact.appendChild(createAvatar(id));
- newContact.appendChild(createDisplayNameParagraph("participant"));
-
- var clElement = contactlist.get(0);
-
- if (resourceJid === APP.xmpp.myResource()
- && $('#contactlist>ul .title')[0].nextSibling.nextSibling) {
- clElement.insertBefore(newContact,
- $('#contactlist>ul .title')[0].nextSibling.nextSibling);
- }
- else {
- clElement.appendChild(newContact);
- }
- updateNumberOfParticipants(1);
- },
-
- /**
- * Removes a contact for the given peer jid.
- *
- * @param peerJid the peerJid corresponding to the contact to remove
- */
- removeContact: function (peerJid) {
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
-
- if (contact && contact.length > 0) {
- var contactlist = $('#contactlist>ul');
-
- contactlist.get(0).removeChild(contact.get(0));
-
- updateNumberOfParticipants(-1);
- }
- },
-
- setVisualNotification: function (show, stopGlowingIn) {
- var glower = $('#contactListButton');
-
- if (show && !notificationInterval) {
- notificationInterval = window.setInterval(function () {
- glower.toggleClass('active glowing');
- }, 800);
- }
- else if (!show && notificationInterval) {
- stopGlowing(glower);
- }
- if (stopGlowingIn) {
- setTimeout(function () {
- stopGlowing(glower);
- }, stopGlowingIn);
- }
- },
-
- setClickable: function (resourceJid, isClickable) {
- var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
- if (isClickable) {
- contact.addClass('clickable');
- } else {
- contact.removeClass('clickable');
- }
- },
-
- onDisplayNameChange: function (peerJid, displayName) {
- if (peerJid === 'localVideoContainer')
- peerJid = APP.xmpp.myJid();
-
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contactName = $('#contactlist #' + resourceJid + '>p');
-
- if (contactName && displayName && displayName.length > 0)
- contactName.html(displayName);
- }
-};
-
-module.exports = ContactList;
},{}],23:[function(require,module,exports){
-var Avatar = require("../../avatar/Avatar");
-var Settings = require("./../../../settings/Settings");
-var UIUtil = require("../../util/UIUtil");
-var languages = require("../../../../service/translation/languages");
-
-function generateLanguagesSelectBox()
-{
- var currentLang = APP.translation.getCurrentLanguage();
- var html = "";
- var langArray = languages.getLanguages();
- for(var i = 0; i < langArray.length; i++)
- {
- var lang = langArray[i];
- html += "";
- html += " ";
-
- }
-
- return html + " ";
-}
-
-
-var SettingsMenu = {
-
- init: function () {
- $("#updateSettings").before(generateLanguagesSelectBox());
- APP.translation.translateElement($("#languages_selectbox"));
- $('#settingsmenu>input').keyup(function(event){
- if(event.keyCode === 13) {//enter
- SettingsMenu.update();
- }
- });
-
- $("#updateSettings").click(function () {
- SettingsMenu.update();
- });
- },
-
- update: function() {
- var newDisplayName = UIUtil.escapeHtml($('#setDisplayName').get(0).value);
- var newEmail = UIUtil.escapeHtml($('#setEmail').get(0).value);
-
- if(newDisplayName) {
- var displayName = Settings.setDisplayName(newDisplayName);
- APP.xmpp.addToPresence("displayName", displayName, true);
- }
-
- var language = $("#languages_selectbox").val();
- APP.translation.setLanguage(language);
- Settings.setLanguage(language);
-
- APP.xmpp.addToPresence("email", newEmail);
- var email = Settings.setEmail(newEmail);
-
-
- Avatar.setUserAvatar(APP.xmpp.myJid(), email);
- },
-
- isVisible: function() {
- return $('#settingsmenu').is(':visible');
- },
-
- setDisplayName: function(newDisplayName) {
- var displayName = Settings.setDisplayName(newDisplayName);
- $('#setDisplayName').get(0).value = displayName;
- },
-
- onDisplayNameChange: function(peerJid, newDisplayName) {
- if(peerJid === 'localVideoContainer' ||
- peerJid === APP.xmpp.myJid()) {
- this.setDisplayName(newDisplayName);
- }
- }
-};
-
-
+
+var numberOfContacts = 0;
+var notificationInterval;
+
+/**
+ * Updates the number of participants in the contact list button and sets
+ * the glow
+ * @param delta indicates whether a new user has joined (1) or someone has
+ * left(-1)
+ */
+function updateNumberOfParticipants(delta) {
+ //when the user is alone we don't show the number of participants
+ if(numberOfContacts === 0) {
+ $("#numberOfParticipants").text('');
+ numberOfContacts += delta;
+ } else if(numberOfContacts !== 0 && !ContactList.isVisible()) {
+ ContactList.setVisualNotification(true);
+ numberOfContacts += delta;
+ $("#numberOfParticipants").text(numberOfContacts);
+ }
+}
+
+/**
+ * Creates the avatar element.
+ *
+ * @return the newly created avatar element
+ */
+function createAvatar(id) {
+ var avatar = document.createElement('img');
+ avatar.className = "icon-avatar avatar";
+ avatar.src = "https://www.gravatar.com/avatar/" + id + "?d=wavatar&size=30";
+
+ return avatar;
+}
+
+/**
+ * Creates the display name paragraph.
+ *
+ * @param displayName the display name to set
+ */
+function createDisplayNameParagraph(key, displayName) {
+ var p = document.createElement('p');
+ if(displayName)
+ p.innerText = displayName;
+ else if(key)
+ {
+ p.setAttribute("data-i18n",key);
+ p.innerText = APP.translation.translateString(key);
+ }
+
+ return p;
+}
+
+
+function stopGlowing(glower) {
+ window.clearInterval(notificationInterval);
+ notificationInterval = false;
+ glower.removeClass('glowing');
+ if (!ContactList.isVisible()) {
+ glower.removeClass('active');
+ }
+}
+
+
+/**
+ * Contact list.
+ */
+var ContactList = {
+ /**
+ * Indicates if the chat is currently visible.
+ *
+ * @return true if the chat is currently visible, false -
+ * otherwise
+ */
+ isVisible: function () {
+ return $('#contactlist').is(":visible");
+ },
+
+ /**
+ * Adds a contact for the given peerJid if such doesn't yet exist.
+ *
+ * @param peerJid the peerJid corresponding to the contact
+ * @param id the user's email or userId used to get the user's avatar
+ */
+ ensureAddContact: function (peerJid, id) {
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
+
+ if (!contact || contact.length <= 0)
+ ContactList.addContact(peerJid, id);
+ },
+
+ /**
+ * Adds a contact for the given peer jid.
+ *
+ * @param peerJid the jid of the contact to add
+ * @param id the email or userId of the user
+ */
+ addContact: function (peerJid, id) {
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contactlist = $('#contactlist>ul');
+
+ var newContact = document.createElement('li');
+ newContact.id = resourceJid;
+ newContact.className = "clickable";
+ newContact.onclick = function (event) {
+ if (event.currentTarget.className === "clickable") {
+ $(ContactList).trigger('contactclicked', [peerJid]);
+ }
+ };
+
+ newContact.appendChild(createAvatar(id));
+ newContact.appendChild(createDisplayNameParagraph("participant"));
+
+ var clElement = contactlist.get(0);
+
+ if (resourceJid === APP.xmpp.myResource()
+ && $('#contactlist>ul .title')[0].nextSibling.nextSibling) {
+ clElement.insertBefore(newContact,
+ $('#contactlist>ul .title')[0].nextSibling.nextSibling);
+ }
+ else {
+ clElement.appendChild(newContact);
+ }
+ updateNumberOfParticipants(1);
+ },
+
+ /**
+ * Removes a contact for the given peer jid.
+ *
+ * @param peerJid the peerJid corresponding to the contact to remove
+ */
+ removeContact: function (peerJid) {
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
+
+ if (contact && contact.length > 0) {
+ var contactlist = $('#contactlist>ul');
+
+ contactlist.get(0).removeChild(contact.get(0));
+
+ updateNumberOfParticipants(-1);
+ }
+ },
+
+ setVisualNotification: function (show, stopGlowingIn) {
+ var glower = $('#contactListButton');
+
+ if (show && !notificationInterval) {
+ notificationInterval = window.setInterval(function () {
+ glower.toggleClass('active glowing');
+ }, 800);
+ }
+ else if (!show && notificationInterval) {
+ stopGlowing(glower);
+ }
+ if (stopGlowingIn) {
+ setTimeout(function () {
+ stopGlowing(glower);
+ }, stopGlowingIn);
+ }
+ },
+
+ setClickable: function (resourceJid, isClickable) {
+ var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
+ if (isClickable) {
+ contact.addClass('clickable');
+ } else {
+ contact.removeClass('clickable');
+ }
+ },
+
+ onDisplayNameChange: function (peerJid, displayName) {
+ if (peerJid === 'localVideoContainer')
+ peerJid = APP.xmpp.myJid();
+
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contactName = $('#contactlist #' + resourceJid + '>p');
+
+ if (contactName && displayName && displayName.length > 0)
+ contactName.html(displayName);
+ }
+};
+
+module.exports = ContactList;
+},{}],24:[function(require,module,exports){
+var Avatar = require("../../avatar/Avatar");
+var Settings = require("./../../../settings/Settings");
+var UIUtil = require("../../util/UIUtil");
+var languages = require("../../../../service/translation/languages");
+
+function generateLanguagesSelectBox()
+{
+ var currentLang = APP.translation.getCurrentLanguage();
+ var html = "";
+ var langArray = languages.getLanguages();
+ for(var i = 0; i < langArray.length; i++)
+ {
+ var lang = langArray[i];
+ html += "";
+ html += " ";
+
+ }
+
+ return html + " ";
+}
+
+
+var SettingsMenu = {
+
+ init: function () {
+ $("#updateSettings").before(generateLanguagesSelectBox());
+ APP.translation.translateElement($("#languages_selectbox"));
+ $('#settingsmenu>input').keyup(function(event){
+ if(event.keyCode === 13) {//enter
+ SettingsMenu.update();
+ }
+ });
+
+ $("#updateSettings").click(function () {
+ SettingsMenu.update();
+ });
+ },
+
+ update: function() {
+ var newDisplayName = UIUtil.escapeHtml($('#setDisplayName').get(0).value);
+ var newEmail = UIUtil.escapeHtml($('#setEmail').get(0).value);
+
+ if(newDisplayName) {
+ var displayName = Settings.setDisplayName(newDisplayName);
+ APP.xmpp.addToPresence("displayName", displayName, true);
+ }
+
+ var language = $("#languages_selectbox").val();
+ APP.translation.setLanguage(language);
+ Settings.setLanguage(language);
+
+ APP.xmpp.addToPresence("email", newEmail);
+ var email = Settings.setEmail(newEmail);
+
+
+ Avatar.setUserAvatar(APP.xmpp.myJid(), email);
+ },
+
+ isVisible: function() {
+ return $('#settingsmenu').is(':visible');
+ },
+
+ setDisplayName: function(newDisplayName) {
+ var displayName = Settings.setDisplayName(newDisplayName);
+ $('#setDisplayName').get(0).value = displayName;
+ },
+
+ onDisplayNameChange: function(peerJid, newDisplayName) {
+ if(peerJid === 'localVideoContainer' ||
+ peerJid === APP.xmpp.myJid()) {
+ this.setDisplayName(newDisplayName);
+ }
+ }
+};
+
+
module.exports = SettingsMenu;
-},{"../../../../service/translation/languages":96,"../../avatar/Avatar":13,"../../util/UIUtil":30,"./../../../settings/Settings":38}],24:[function(require,module,exports){
-var PanelToggler = require("../side_pannels/SidePanelToggler");
-
-var buttonHandlers = {
- "bottom_toolbar_contact_list": function () {
- BottomToolbar.toggleContactList();
- },
- "bottom_toolbar_film_strip": function () {
- BottomToolbar.toggleFilmStrip();
- },
- "bottom_toolbar_chat": function () {
- BottomToolbar.toggleChat();
- }
-};
-
-var BottomToolbar = (function (my) {
- my.init = function () {
- for(var k in buttonHandlers)
- $("#" + k).click(buttonHandlers[k]);
- };
-
- my.toggleChat = function() {
- PanelToggler.toggleChat();
- };
-
- my.toggleContactList = function() {
- PanelToggler.toggleContactList();
- };
-
- my.toggleFilmStrip = function() {
- var filmstrip = $("#remoteVideos");
- filmstrip.toggleClass("hidden");
- };
-
- $(document).bind("remotevideo.resized", function (event, width, height) {
- var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
-
- $('#bottomToolbar').css({bottom: bottom + 'px'});
- });
-
- return my;
-}(BottomToolbar || {}));
-
-module.exports = BottomToolbar;
-
-},{"../side_pannels/SidePanelToggler":17}],25:[function(require,module,exports){
-/* global APP,$, buttonClick, config, lockRoom,
- setSharedKey, Util */
-var messageHandler = require("../util/MessageHandler");
-var BottomToolbar = require("./BottomToolbar");
-var Prezi = require("../prezi/Prezi");
-var Etherpad = require("../etherpad/Etherpad");
-var PanelToggler = require("../side_pannels/SidePanelToggler");
-var Authentication = require("../authentication/Authentication");
-var UIUtil = require("../util/UIUtil");
-var AuthenticationEvents
- = require("../../../service/authentication/AuthenticationEvents");
-
-var roomUrl = null;
-var sharedKey = '';
-var UI = null;
-
-var buttonHandlers =
-{
- "toolbar_button_mute": function () {
- return APP.UI.toggleAudio();
- },
- "toolbar_button_camera": function () {
- return APP.UI.toggleVideo();
- },
- /*"toolbar_button_authentication": function () {
- return Toolbar.authenticateClicked();
- },*/
- "toolbar_button_record": function () {
- return toggleRecording();
- },
- "toolbar_button_security": function () {
- return Toolbar.openLockDialog();
- },
- "toolbar_button_link": function () {
- return Toolbar.openLinkDialog();
- },
- "toolbar_button_chat": function () {
- return BottomToolbar.toggleChat();
- },
- "toolbar_button_prezi": function () {
- return Prezi.openPreziDialog();
- },
- "toolbar_button_etherpad": function () {
- return Etherpad.toggleEtherpad(0);
- },
- "toolbar_button_desktopsharing": function () {
- return APP.desktopsharing.toggleScreenSharing();
- },
- "toolbar_button_fullScreen": function()
- {
- UIUtil.buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");
- return Toolbar.toggleFullScreen();
- },
- "toolbar_button_sip": function () {
- return callSipButtonClicked();
- },
- "toolbar_button_settings": function () {
- PanelToggler.toggleSettingsMenu();
- },
- "toolbar_button_hangup": function () {
- return hangup();
- },
- "toolbar_button_login": function () {
- Toolbar.authenticateClicked();
- },
- "toolbar_button_logout": function () {
- // Ask for confirmation
- messageHandler.openTwoButtonDialog(
- "dialog.logoutTitle",
- null,
- "dialog.logoutQuestion",
- null,
- false,
- "dialog.Yes",
- function (evt, yes) {
- if (yes) {
- APP.xmpp.logout(function (url) {
- if (url) {
- window.location.href = url;
- } else {
- hangup();
- }
- });
- }
- });
- }
-};
-
-function hangup() {
- APP.xmpp.disposeConference();
- if(config.enableWelcomePage)
- {
- setTimeout(function()
- {
- window.localStorage.welcomePageDisabled = false;
- window.location.pathname = "/";
- }, 10000);
-
- }
-
- var title = APP.translation.generateTranslatonHTML(
- "dialog.sessTerminated");
- var msg = APP.translation.generateTranslatonHTML(
- "dialog.hungUp");
- var button = APP.translation.generateTranslatonHTML(
- "dialog.joinAgain");
- var buttons = [];
- buttons.push({title: button, value: true});
-
- UI.messageHandler.openDialog(
- title,
- msg,
- true,
- buttons,
- function(event, value, message, formVals)
- {
- window.location.reload();
- return false;
- }
- );
-}
-
-/**
- * Starts or stops the recording for the conference.
- */
-
-function toggleRecording() {
- APP.xmpp.toggleRecording(function (callback) {
- var msg = APP.translation.generateTranslatonHTML(
- "dialog.recordingToken");
- var token = APP.translation.translateString("dialog.token");
- APP.UI.messageHandler.openTwoButtonDialog(null, null, null,
- '' + msg + ' ' +
- ' ',
- false,
- "dialog.Save",
- function (e, v, m, f) {
- if (v) {
- var token = f.recordingToken;
-
- if (token) {
- callback(UIUtil.escapeHtml(token));
- }
- }
- },
- null,
- function () { },
- ':input:first'
- );
- }, Toolbar.setRecordingButtonState, Toolbar.setRecordingButtonState);
-}
-
-/**
- * Locks / unlocks the room.
- */
-function lockRoom(lock) {
- var currentSharedKey = '';
- if (lock)
- currentSharedKey = sharedKey;
-
- APP.xmpp.lockRoom(currentSharedKey, function (res) {
- // password is required
- if (sharedKey)
- {
- console.log('set room password');
- Toolbar.lockLockButton();
- }
- else
- {
- console.log('removed room password');
- Toolbar.unlockLockButton();
- }
- }, function (err) {
- console.warn('setting password failed', err);
- messageHandler.showError("dialog.lockTitle",
- "dialog.lockMessage");
- Toolbar.setSharedKey('');
- }, function () {
- console.warn('room passwords not supported');
- messageHandler.showError("dialog.warning",
- "dialog.passwordNotSupported");
- Toolbar.setSharedKey('');
- });
-};
-
-/**
- * Invite participants to conference.
- */
-function inviteParticipants() {
- if (roomUrl === null)
- return;
-
- var sharedKeyText = "";
- if (sharedKey && sharedKey.length > 0) {
- sharedKeyText =
- APP.translation.translateString("email.sharedKey",
- {sharedKey: sharedKey});
- sharedKeyText = sharedKeyText.replace(/\n/g, "%0D%0A");
- }
-
- var supportedBrowsers = "Chromium, Google Chrome " +
- APP.translation.translateString("email.and") + " Opera";
- var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
- var subject = APP.translation.translateString("email.subject",
- {appName:interfaceConfig.APP_NAME, conferenceName: conferenceName});
- var body = APP.translation.translateString("email.body",
- {appName:interfaceConfig.APP_NAME, sharedKeyText: sharedKeyText,
- roomUrl: roomUrl, supportedBrowsers: supportedBrowsers});
- body = body.replace(/\n/g, "%0D%0A");
-
- if (window.localStorage.displayname) {
- body += "%0D%0A%0D%0A" + window.localStorage.displayname;
- }
-
- if (interfaceConfig.INVITATION_POWERED_BY) {
- body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
- }
-
- window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
-}
-
-function callSipButtonClicked()
-{
- var defaultNumber
- = config.defaultSipNumber ? config.defaultSipNumber : '';
-
- var sipMsg = APP.translation.generateTranslatonHTML(
- "dialog.sipMsg");
- messageHandler.openTwoButtonDialog(null, null, null,
- '' + sipMsg + ' ' +
- ' ',
- false,
- "dialog.Dial",
- function (e, v, m, f) {
- if (v) {
- var numberInput = f.sipNumber;
- if (numberInput) {
- APP.xmpp.dial(
- numberInput, 'fromnumber', UI.getRoomName(), sharedKey);
- }
- }
- },
- null,
- ':input:first'
- );
-}
-
-var Toolbar = (function (my) {
-
- my.init = function (ui) {
- for(var k in buttonHandlers)
- $("#" + k).click(buttonHandlers[k]);
- UI = ui;
- // Update login info
- APP.xmpp.addListener(
- AuthenticationEvents.IDENTITY_UPDATED,
- function (authenticationEnabled, userIdentity) {
-
- var loggedIn = false;
- if (userIdentity) {
- loggedIn = true;
- }
-
- Toolbar.showAuthenticateButton(authenticationEnabled);
-
- if (authenticationEnabled) {
- Toolbar.setAuthenticatedIdentity(userIdentity);
-
- Toolbar.showLoginButton(!loggedIn);
- Toolbar.showLogoutButton(loggedIn);
- }
- }
- );
- },
-
- /**
- * Sets shared key
- * @param sKey the shared key
- */
- my.setSharedKey = function (sKey) {
- sharedKey = sKey;
- };
-
- my.authenticateClicked = function () {
- Authentication.focusAuthenticationWindow();
- if (!APP.xmpp.isExternalAuthEnabled()) {
- Authentication.xmppAuthenticate();
- return;
- }
- // Get authentication URL
- if (!APP.xmpp.getMUCJoined()) {
- APP.xmpp.getLoginUrl(UI.getRoomName(), function (url) {
- // If conference has not been started yet - redirect to login page
- window.location.href = url;
- });
- } else {
- APP.xmpp.getPopupLoginUrl(UI.getRoomName(), function (url) {
- // Otherwise - open popup with authentication URL
- var authenticationWindow = Authentication.createAuthenticationWindow(
- function () {
- // On popup closed - retry room allocation
- APP.xmpp.allocateConferenceFocus(
- APP.UI.getRoomName(),
- function () { console.info("AUTH DONE"); }
- );
- }, url);
- if (!authenticationWindow) {
- messageHandler.openMessageDialog(
- null, "dialog.popupError");
- }
- });
- }
- };
-
- /**
- * Updates the room invite url.
- */
- my.updateRoomUrl = function (newRoomUrl) {
- roomUrl = newRoomUrl;
-
- // If the invite dialog has been already opened we update the information.
- var inviteLink = document.getElementById('inviteLinkRef');
- if (inviteLink) {
- inviteLink.value = roomUrl;
- inviteLink.select();
- document.getElementById('jqi_state0_buttonInvite').disabled = false;
- }
- };
-
- /**
- * Disables and enables some of the buttons.
- */
- my.setupButtonsFromConfig = function () {
- if (config.disablePrezi)
- {
- $("#prezi_button").css({display: "none"});
- }
- };
-
- /**
- * Opens the lock room dialog.
- */
- my.openLockDialog = function () {
- // Only the focus is able to set a shared key.
- if (!APP.xmpp.isModerator()) {
- if (sharedKey) {
- messageHandler.openMessageDialog(null,
- "dialog.passwordError");
- } else {
- messageHandler.openMessageDialog(null, "dialog.passwordError2");
- }
- } else {
- if (sharedKey) {
- messageHandler.openTwoButtonDialog(null, null,
- "dialog.passwordCheck",
- null,
- false,
- "dialog.Remove",
- function (e, v) {
- if (v) {
- Toolbar.setSharedKey('');
- lockRoom(false);
- }
- });
- } else {
- var msg = APP.translation.generateTranslatonHTML(
- "dialog.passwordMsg");
- var yourPassword = APP.translation.translateString(
- "dialog.yourPassword");
- messageHandler.openTwoButtonDialog(null, null, null,
- '' + msg + ' ' +
- ' ',
- false,
- "dialog.Save",
- function (e, v, m, f) {
- if (v) {
- var lockKey = f.lockKey;
-
- if (lockKey) {
- Toolbar.setSharedKey(
- UIUtil.escapeHtml(lockKey));
- lockRoom(true);
- }
- }
- },
- null, null, 'input:first'
- );
- }
- }
- };
-
- /**
- * Opens the invite link dialog.
- */
- my.openLinkDialog = function () {
- var inviteAttreibutes;
-
- if (roomUrl === null) {
- inviteAttreibutes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
- APP.translation.translateString("roomUrlDefaultMsg") + '"';
- } else {
- inviteAttreibutes = "value=\"" + encodeURI(roomUrl) + "\"";
- }
- messageHandler.openTwoButtonDialog("dialog.shareLink",
- null, null,
- ' ',
- false,
- "dialog.Invite",
- function (e, v) {
- if (v) {
- if (roomUrl) {
- inviteParticipants();
- }
- }
- },
- function () {
- if (roomUrl) {
- document.getElementById('inviteLinkRef').select();
- } else {
- document.getElementById('jqi_state0_buttonInvite')
- .disabled = true;
- }
- }
- );
- };
-
- /**
- * Opens the settings dialog.
- * FIXME: not used ?
- */
- my.openSettingsDialog = function () {
- var settings1 = APP.translation.generateTranslatonHTML(
- "dialog.settings1");
- var settings2 = APP.translation.generateTranslatonHTML(
- "dialog.settings2");
- var settings3 = APP.translation.generateTranslatonHTML(
- "dialog.settings3");
-
- var yourPassword = APP.translation.translateString(
- "dialog.yourPassword");
-
- messageHandler.openTwoButtonDialog(null,
- '' + settings1 + ' ' +
- ' ' +
- settings2 + ' ' +
- ' ' +
- settings3 +
- ' ',
- null,
- null,
- false,
- "dialog.Save",
- function () {
- document.getElementById('lockKey').focus();
- },
- function (e, v) {
- if (v) {
- if ($('#initMuted').is(":checked")) {
- // it is checked
- }
-
- if ($('#requireNicknames').is(":checked")) {
- // it is checked
- }
- /*
- var lockKey = document.getElementById('lockKey');
-
- if (lockKey.value) {
- setSharedKey(lockKey.value);
- lockRoom(true);
- }
- */
- }
- }
- );
- };
-
- /**
- * Toggles the application in and out of full screen mode
- * (a.k.a. presentation mode in Chrome).
- */
- my.toggleFullScreen = function () {
- var fsElement = document.documentElement;
-
- if (!document.mozFullScreen && !document.webkitIsFullScreen) {
- //Enter Full Screen
- if (fsElement.mozRequestFullScreen) {
- fsElement.mozRequestFullScreen();
- }
- else {
- fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
- }
- } else {
- //Exit Full Screen
- if (document.mozCancelFullScreen) {
- document.mozCancelFullScreen();
- } else {
- document.webkitCancelFullScreen();
- }
- }
- };
- /**
- * Unlocks the lock button state.
- */
- my.unlockLockButton = function () {
- if ($("#lockIcon").hasClass("icon-security-locked"))
- UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked");
- };
- /**
- * Updates the lock button state to locked.
- */
- my.lockLockButton = function () {
- if ($("#lockIcon").hasClass("icon-security"))
- UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked");
- };
-
- /**
- * Shows or hides authentication button
- * @param show true to show or false to hide
- */
- my.showAuthenticateButton = function (show) {
- if (show) {
- $('#authentication').css({display: "inline"});
- }
- else {
- $('#authentication').css({display: "none"});
- }
- };
-
- // Shows or hides the 'recording' button.
- my.showRecordingButton = function (show) {
- if (!config.enableRecording) {
- return;
- }
-
- if (show) {
- $('#recording').css({display: "inline"});
- }
- else {
- $('#recording').css({display: "none"});
- }
- };
-
- // Sets the state of the recording button
- my.setRecordingButtonState = function (isRecording) {
- if (isRecording) {
- $('#recordButton').removeClass("icon-recEnable");
- $('#recordButton').addClass("icon-recEnable active");
- } else {
- $('#recordButton').removeClass("icon-recEnable active");
- $('#recordButton').addClass("icon-recEnable");
- }
- };
-
- // Shows or hides SIP calls button
- my.showSipCallButton = function (show) {
- if (APP.xmpp.isSipGatewayEnabled() && show) {
- $('#sipCallButton').css({display: "inline-block"});
- } else {
- $('#sipCallButton').css({display: "none"});
- }
- };
-
- /**
- * Displays user authenticated identity name(login).
- * @param authIdentity identity name to be displayed.
- */
- my.setAuthenticatedIdentity = function (authIdentity) {
- if (authIdentity) {
- $('#toolbar_auth_identity').css({display: "list-item"});
- $('#toolbar_auth_identity').text(authIdentity);
- } else {
- $('#toolbar_auth_identity').css({display: "none"});
- }
- };
-
- /**
- * Shows/hides login button.
- * @param show true to show
- */
- my.showLoginButton = function (show) {
- if (show) {
- $('#toolbar_button_login').css({display: "list-item"});
- } else {
- $('#toolbar_button_login').css({display: "none"});
- }
- };
-
- /**
- * Shows/hides logout button.
- * @param show true to show
- */
- my.showLogoutButton = function (show) {
- if (show) {
- $('#toolbar_button_logout').css({display: "list-item"});
- } else {
- $('#toolbar_button_logout').css({display: "none"});
- }
- };
-
- /**
- * Sets the state of the button. The button has blue glow if desktop
- * streaming is active.
- * @param active the state of the desktop streaming.
- */
- my.changeDesktopSharingButtonState = function (active) {
- var button = $("#desktopsharing > a");
- if (active)
- {
- button.addClass("glow");
- }
- else
- {
- button.removeClass("glow");
- }
- };
-
- return my;
-}(Toolbar || {}));
+},{"../../../../service/translation/languages":97,"../../avatar/Avatar":14,"../../util/UIUtil":31,"./../../../settings/Settings":39}],25:[function(require,module,exports){
+var PanelToggler = require("../side_pannels/SidePanelToggler");
+
+var buttonHandlers = {
+ "bottom_toolbar_contact_list": function () {
+ BottomToolbar.toggleContactList();
+ },
+ "bottom_toolbar_film_strip": function () {
+ BottomToolbar.toggleFilmStrip();
+ },
+ "bottom_toolbar_chat": function () {
+ BottomToolbar.toggleChat();
+ }
+};
+
+var BottomToolbar = (function (my) {
+ my.init = function () {
+ for(var k in buttonHandlers)
+ $("#" + k).click(buttonHandlers[k]);
+ };
+
+ my.toggleChat = function() {
+ PanelToggler.toggleChat();
+ };
+
+ my.toggleContactList = function() {
+ PanelToggler.toggleContactList();
+ };
+
+ my.toggleFilmStrip = function() {
+ var filmstrip = $("#remoteVideos");
+ filmstrip.toggleClass("hidden");
+ };
+
+ $(document).bind("remotevideo.resized", function (event, width, height) {
+ var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
+
+ $('#bottomToolbar').css({bottom: bottom + 'px'});
+ });
+
+ return my;
+}(BottomToolbar || {}));
+
+module.exports = BottomToolbar;
+},{"../side_pannels/SidePanelToggler":18}],26:[function(require,module,exports){
+/* global APP,$, buttonClick, config, lockRoom,
+ setSharedKey, Util */
+var messageHandler = require("../util/MessageHandler");
+var BottomToolbar = require("./BottomToolbar");
+var Prezi = require("../prezi/Prezi");
+var Etherpad = require("../etherpad/Etherpad");
+var PanelToggler = require("../side_pannels/SidePanelToggler");
+var Authentication = require("../authentication/Authentication");
+var UIUtil = require("../util/UIUtil");
+var AuthenticationEvents
+ = require("../../../service/authentication/AuthenticationEvents");
+
+var roomUrl = null;
+var sharedKey = '';
+var UI = null;
+
+var buttonHandlers =
+{
+ "toolbar_button_mute": function () {
+ return APP.UI.toggleAudio();
+ },
+ "toolbar_button_camera": function () {
+ return APP.UI.toggleVideo();
+ },
+ /*"toolbar_button_authentication": function () {
+ return Toolbar.authenticateClicked();
+ },*/
+ "toolbar_button_record": function () {
+ return toggleRecording();
+ },
+ "toolbar_button_security": function () {
+ return Toolbar.openLockDialog();
+ },
+ "toolbar_button_link": function () {
+ return Toolbar.openLinkDialog();
+ },
+ "toolbar_button_chat": function () {
+ return BottomToolbar.toggleChat();
+ },
+ "toolbar_button_prezi": function () {
+ return Prezi.openPreziDialog();
+ },
+ "toolbar_button_etherpad": function () {
+ return Etherpad.toggleEtherpad(0);
+ },
+ "toolbar_button_desktopsharing": function () {
+ return APP.desktopsharing.toggleScreenSharing();
+ },
+ "toolbar_button_fullScreen": function()
+ {
+ UIUtil.buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");
+ return Toolbar.toggleFullScreen();
+ },
+ "toolbar_button_sip": function () {
+ return callSipButtonClicked();
+ },
+ "toolbar_button_settings": function () {
+ PanelToggler.toggleSettingsMenu();
+ },
+ "toolbar_button_hangup": function () {
+ return hangup();
+ },
+ "toolbar_button_login": function () {
+ Toolbar.authenticateClicked();
+ },
+ "toolbar_button_logout": function () {
+ // Ask for confirmation
+ messageHandler.openTwoButtonDialog(
+ "dialog.logoutTitle",
+ null,
+ "dialog.logoutQuestion",
+ null,
+ false,
+ "dialog.Yes",
+ function (evt, yes) {
+ if (yes) {
+ APP.xmpp.logout(function (url) {
+ if (url) {
+ window.location.href = url;
+ } else {
+ hangup();
+ }
+ });
+ }
+ });
+ }
+};
+
+function hangup() {
+ APP.xmpp.disposeConference();
+ if(config.enableWelcomePage)
+ {
+ setTimeout(function()
+ {
+ window.localStorage.welcomePageDisabled = false;
+ window.location.pathname = "/";
+ }, 10000);
+
+ }
+
+ var title = APP.translation.generateTranslatonHTML(
+ "dialog.sessTerminated");
+ var msg = APP.translation.generateTranslatonHTML(
+ "dialog.hungUp");
+ var button = APP.translation.generateTranslatonHTML(
+ "dialog.joinAgain");
+ var buttons = [];
+ buttons.push({title: button, value: true});
+
+ UI.messageHandler.openDialog(
+ title,
+ msg,
+ true,
+ buttons,
+ function(event, value, message, formVals)
+ {
+ window.location.reload();
+ return false;
+ }
+ );
+}
+
+/**
+ * Starts or stops the recording for the conference.
+ */
+
+function toggleRecording() {
+ APP.xmpp.toggleRecording(function (callback) {
+ var msg = APP.translation.generateTranslatonHTML(
+ "dialog.recordingToken");
+ var token = APP.translation.translateString("dialog.token");
+ APP.UI.messageHandler.openTwoButtonDialog(null, null, null,
+ '' + msg + ' ' +
+ ' ',
+ false,
+ "dialog.Save",
+ function (e, v, m, f) {
+ if (v) {
+ var token = f.recordingToken;
+
+ if (token) {
+ callback(UIUtil.escapeHtml(token));
+ }
+ }
+ },
+ null,
+ function () { },
+ ':input:first'
+ );
+ }, Toolbar.setRecordingButtonState, Toolbar.setRecordingButtonState);
+}
+
+/**
+ * Locks / unlocks the room.
+ */
+function lockRoom(lock) {
+ var currentSharedKey = '';
+ if (lock)
+ currentSharedKey = sharedKey;
+
+ APP.xmpp.lockRoom(currentSharedKey, function (res) {
+ // password is required
+ if (sharedKey)
+ {
+ console.log('set room password');
+ Toolbar.lockLockButton();
+ }
+ else
+ {
+ console.log('removed room password');
+ Toolbar.unlockLockButton();
+ }
+ }, function (err) {
+ console.warn('setting password failed', err);
+ messageHandler.showError("dialog.lockTitle",
+ "dialog.lockMessage");
+ Toolbar.setSharedKey('');
+ }, function () {
+ console.warn('room passwords not supported');
+ messageHandler.showError("dialog.warning",
+ "dialog.passwordNotSupported");
+ Toolbar.setSharedKey('');
+ });
+};
+
+/**
+ * Invite participants to conference.
+ */
+function inviteParticipants() {
+ if (roomUrl === null)
+ return;
+
+ var sharedKeyText = "";
+ if (sharedKey && sharedKey.length > 0) {
+ sharedKeyText =
+ APP.translation.translateString("email.sharedKey",
+ {sharedKey: sharedKey});
+ sharedKeyText = sharedKeyText.replace(/\n/g, "%0D%0A");
+ }
+
+ var supportedBrowsers = "Chromium, Google Chrome " +
+ APP.translation.translateString("email.and") + " Opera";
+ var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
+ var subject = APP.translation.translateString("email.subject",
+ {appName:interfaceConfig.APP_NAME, conferenceName: conferenceName});
+ var body = APP.translation.translateString("email.body",
+ {appName:interfaceConfig.APP_NAME, sharedKeyText: sharedKeyText,
+ roomUrl: roomUrl, supportedBrowsers: supportedBrowsers});
+ body = body.replace(/\n/g, "%0D%0A");
+
+ if (window.localStorage.displayname) {
+ body += "%0D%0A%0D%0A" + window.localStorage.displayname;
+ }
+
+ if (interfaceConfig.INVITATION_POWERED_BY) {
+ body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
+ }
+
+ window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
+}
+
+function callSipButtonClicked()
+{
+ var defaultNumber
+ = config.defaultSipNumber ? config.defaultSipNumber : '';
+
+ var sipMsg = APP.translation.generateTranslatonHTML(
+ "dialog.sipMsg");
+ messageHandler.openTwoButtonDialog(null, null, null,
+ '' + sipMsg + ' ' +
+ ' ',
+ false,
+ "dialog.Dial",
+ function (e, v, m, f) {
+ if (v) {
+ var numberInput = f.sipNumber;
+ if (numberInput) {
+ APP.xmpp.dial(
+ numberInput, 'fromnumber', UI.getRoomName(), sharedKey);
+ }
+ }
+ },
+ null,
+ ':input:first'
+ );
+}
+
+var Toolbar = (function (my) {
+
+ my.init = function (ui) {
+ for(var k in buttonHandlers)
+ $("#" + k).click(buttonHandlers[k]);
+ UI = ui;
+ // Update login info
+ APP.xmpp.addListener(
+ AuthenticationEvents.IDENTITY_UPDATED,
+ function (authenticationEnabled, userIdentity) {
+
+ var loggedIn = false;
+ if (userIdentity) {
+ loggedIn = true;
+ }
+
+ Toolbar.showAuthenticateButton(authenticationEnabled);
+
+ if (authenticationEnabled) {
+ Toolbar.setAuthenticatedIdentity(userIdentity);
+
+ Toolbar.showLoginButton(!loggedIn);
+ Toolbar.showLogoutButton(loggedIn);
+ }
+ }
+ );
+ },
+
+ /**
+ * Sets shared key
+ * @param sKey the shared key
+ */
+ my.setSharedKey = function (sKey) {
+ sharedKey = sKey;
+ };
+
+ my.authenticateClicked = function () {
+ Authentication.focusAuthenticationWindow();
+ if (!APP.xmpp.isExternalAuthEnabled()) {
+ Authentication.xmppAuthenticate();
+ return;
+ }
+ // Get authentication URL
+ if (!APP.xmpp.getMUCJoined()) {
+ APP.xmpp.getLoginUrl(UI.getRoomName(), function (url) {
+ // If conference has not been started yet - redirect to login page
+ window.location.href = url;
+ });
+ } else {
+ APP.xmpp.getPopupLoginUrl(UI.getRoomName(), function (url) {
+ // Otherwise - open popup with authentication URL
+ var authenticationWindow = Authentication.createAuthenticationWindow(
+ function () {
+ // On popup closed - retry room allocation
+ APP.xmpp.allocateConferenceFocus(
+ APP.UI.getRoomName(),
+ function () { console.info("AUTH DONE"); }
+ );
+ }, url);
+ if (!authenticationWindow) {
+ messageHandler.openMessageDialog(
+ null, "dialog.popupError");
+ }
+ });
+ }
+ };
+
+ /**
+ * Updates the room invite url.
+ */
+ my.updateRoomUrl = function (newRoomUrl) {
+ roomUrl = newRoomUrl;
+
+ // If the invite dialog has been already opened we update the information.
+ var inviteLink = document.getElementById('inviteLinkRef');
+ if (inviteLink) {
+ inviteLink.value = roomUrl;
+ inviteLink.select();
+ document.getElementById('jqi_state0_buttonInvite').disabled = false;
+ }
+ };
+
+ /**
+ * Disables and enables some of the buttons.
+ */
+ my.setupButtonsFromConfig = function () {
+ if (config.disablePrezi)
+ {
+ $("#prezi_button").css({display: "none"});
+ }
+ };
+
+ /**
+ * Opens the lock room dialog.
+ */
+ my.openLockDialog = function () {
+ // Only the focus is able to set a shared key.
+ if (!APP.xmpp.isModerator()) {
+ if (sharedKey) {
+ messageHandler.openMessageDialog(null,
+ "dialog.passwordError");
+ } else {
+ messageHandler.openMessageDialog(null, "dialog.passwordError2");
+ }
+ } else {
+ if (sharedKey) {
+ messageHandler.openTwoButtonDialog(null, null,
+ "dialog.passwordCheck",
+ null,
+ false,
+ "dialog.Remove",
+ function (e, v) {
+ if (v) {
+ Toolbar.setSharedKey('');
+ lockRoom(false);
+ }
+ });
+ } else {
+ var msg = APP.translation.generateTranslatonHTML(
+ "dialog.passwordMsg");
+ var yourPassword = APP.translation.translateString(
+ "dialog.yourPassword");
+ messageHandler.openTwoButtonDialog(null, null, null,
+ '' + msg + ' ' +
+ ' ',
+ false,
+ "dialog.Save",
+ function (e, v, m, f) {
+ if (v) {
+ var lockKey = f.lockKey;
+
+ if (lockKey) {
+ Toolbar.setSharedKey(
+ UIUtil.escapeHtml(lockKey));
+ lockRoom(true);
+ }
+ }
+ },
+ null, null, 'input:first'
+ );
+ }
+ }
+ };
+
+ /**
+ * Opens the invite link dialog.
+ */
+ my.openLinkDialog = function () {
+ var inviteAttreibutes;
+
+ if (roomUrl === null) {
+ inviteAttreibutes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
+ APP.translation.translateString("roomUrlDefaultMsg") + '"';
+ } else {
+ inviteAttreibutes = "value=\"" + encodeURI(roomUrl) + "\"";
+ }
+ messageHandler.openTwoButtonDialog("dialog.shareLink",
+ null, null,
+ ' ',
+ false,
+ "dialog.Invite",
+ function (e, v) {
+ if (v) {
+ if (roomUrl) {
+ inviteParticipants();
+ }
+ }
+ },
+ function () {
+ if (roomUrl) {
+ document.getElementById('inviteLinkRef').select();
+ } else {
+ document.getElementById('jqi_state0_buttonInvite')
+ .disabled = true;
+ }
+ }
+ );
+ };
+
+ /**
+ * Opens the settings dialog.
+ * FIXME: not used ?
+ */
+ my.openSettingsDialog = function () {
+ var settings1 = APP.translation.generateTranslatonHTML(
+ "dialog.settings1");
+ var settings2 = APP.translation.generateTranslatonHTML(
+ "dialog.settings2");
+ var settings3 = APP.translation.generateTranslatonHTML(
+ "dialog.settings3");
+
+ var yourPassword = APP.translation.translateString(
+ "dialog.yourPassword");
+
+ messageHandler.openTwoButtonDialog(null,
+ '' + settings1 + ' ' +
+ ' ' +
+ settings2 + ' ' +
+ ' ' +
+ settings3 +
+ ' ',
+ null,
+ null,
+ false,
+ "dialog.Save",
+ function () {
+ document.getElementById('lockKey').focus();
+ },
+ function (e, v) {
+ if (v) {
+ if ($('#initMuted').is(":checked")) {
+ // it is checked
+ }
+
+ if ($('#requireNicknames').is(":checked")) {
+ // it is checked
+ }
+ /*
+ var lockKey = document.getElementById('lockKey');
+
+ if (lockKey.value) {
+ setSharedKey(lockKey.value);
+ lockRoom(true);
+ }
+ */
+ }
+ }
+ );
+ };
+
+ /**
+ * Toggles the application in and out of full screen mode
+ * (a.k.a. presentation mode in Chrome).
+ */
+ my.toggleFullScreen = function () {
+ var fsElement = document.documentElement;
+
+ if (!document.mozFullScreen && !document.webkitIsFullScreen) {
+ //Enter Full Screen
+ if (fsElement.mozRequestFullScreen) {
+ fsElement.mozRequestFullScreen();
+ }
+ else {
+ fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
+ }
+ } else {
+ //Exit Full Screen
+ if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else {
+ document.webkitCancelFullScreen();
+ }
+ }
+ };
+ /**
+ * Unlocks the lock button state.
+ */
+ my.unlockLockButton = function () {
+ if ($("#lockIcon").hasClass("icon-security-locked"))
+ UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked");
+ };
+ /**
+ * Updates the lock button state to locked.
+ */
+ my.lockLockButton = function () {
+ if ($("#lockIcon").hasClass("icon-security"))
+ UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked");
+ };
+
+ /**
+ * Shows or hides authentication button
+ * @param show true to show or false to hide
+ */
+ my.showAuthenticateButton = function (show) {
+ if (show) {
+ $('#authentication').css({display: "inline"});
+ }
+ else {
+ $('#authentication').css({display: "none"});
+ }
+ };
+
+ // Shows or hides the 'recording' button.
+ my.showRecordingButton = function (show) {
+ if (!config.enableRecording) {
+ return;
+ }
+
+ if (show) {
+ $('#recording').css({display: "inline"});
+ }
+ else {
+ $('#recording').css({display: "none"});
+ }
+ };
+
+ // Sets the state of the recording button
+ my.setRecordingButtonState = function (isRecording) {
+ if (isRecording) {
+ $('#recordButton').removeClass("icon-recEnable");
+ $('#recordButton').addClass("icon-recEnable active");
+ } else {
+ $('#recordButton').removeClass("icon-recEnable active");
+ $('#recordButton').addClass("icon-recEnable");
+ }
+ };
+
+ // Shows or hides SIP calls button
+ my.showSipCallButton = function (show) {
+ if (APP.xmpp.isSipGatewayEnabled() && show) {
+ $('#sipCallButton').css({display: "inline-block"});
+ } else {
+ $('#sipCallButton').css({display: "none"});
+ }
+ };
+
+ /**
+ * Displays user authenticated identity name(login).
+ * @param authIdentity identity name to be displayed.
+ */
+ my.setAuthenticatedIdentity = function (authIdentity) {
+ if (authIdentity) {
+ $('#toolbar_auth_identity').css({display: "list-item"});
+ $('#toolbar_auth_identity').text(authIdentity);
+ } else {
+ $('#toolbar_auth_identity').css({display: "none"});
+ }
+ };
+
+ /**
+ * Shows/hides login button.
+ * @param show true to show
+ */
+ my.showLoginButton = function (show) {
+ if (show) {
+ $('#toolbar_button_login').css({display: "list-item"});
+ } else {
+ $('#toolbar_button_login').css({display: "none"});
+ }
+ };
+
+ /**
+ * Shows/hides logout button.
+ * @param show true to show
+ */
+ my.showLogoutButton = function (show) {
+ if (show) {
+ $('#toolbar_button_logout').css({display: "list-item"});
+ } else {
+ $('#toolbar_button_logout').css({display: "none"});
+ }
+ };
+
+ /**
+ * Sets the state of the button. The button has blue glow if desktop
+ * streaming is active.
+ * @param active the state of the desktop streaming.
+ */
+ my.changeDesktopSharingButtonState = function (active) {
+ var button = $("#desktopsharing > a");
+ if (active)
+ {
+ button.addClass("glow");
+ }
+ else
+ {
+ button.removeClass("glow");
+ }
+ };
+
+ return my;
+}(Toolbar || {}));
+
module.exports = Toolbar;
-},{"../../../service/authentication/AuthenticationEvents":93,"../authentication/Authentication":11,"../etherpad/Etherpad":14,"../prezi/Prezi":15,"../side_pannels/SidePanelToggler":17,"../util/MessageHandler":28,"../util/UIUtil":30,"./BottomToolbar":24}],26:[function(require,module,exports){
-/* global $, interfaceConfig, Moderator, DesktopStreaming.showDesktopSharingButton */
-
-var toolbarTimeoutObject,
- toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
-
-function showDesktopSharingButton() {
- if (APP.desktopsharing.isDesktopSharingEnabled()) {
- $('#desktopsharing').css({display: "inline"});
- } else {
- $('#desktopsharing').css({display: "none"});
- }
-}
-
-/**
- * Hides the toolbar.
- */
-function hideToolbar() {
- var header = $("#header"),
- bottomToolbar = $("#bottomToolbar");
- var isToolbarHover = false;
- header.find('*').each(function () {
- var id = $(this).attr('id');
- if ($("#" + id + ":hover").length > 0) {
- isToolbarHover = true;
- }
- });
- if ($("#bottomToolbar:hover").length > 0) {
- isToolbarHover = true;
- }
-
- clearTimeout(toolbarTimeoutObject);
- toolbarTimeoutObject = null;
-
- if (!isToolbarHover) {
- header.hide("slide", { direction: "up", duration: 300});
- $('#subject').animate({top: "-=40"}, 300);
- if ($("#remoteVideos").hasClass("hidden")) {
- bottomToolbar.hide(
- "slide", {direction: "right", duration: 300});
- }
- }
- else {
- toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
- }
-}
-
-var ToolbarToggler = {
- /**
- * Shows the main toolbar.
- */
- showToolbar: function () {
- var header = $("#header"),
- bottomToolbar = $("#bottomToolbar");
- if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
- header.show("slide", { direction: "up", duration: 300});
- $('#subject').animate({top: "+=40"}, 300);
- if (!bottomToolbar.is(":visible")) {
- bottomToolbar.show(
- "slide", {direction: "right", duration: 300});
- }
-
- if (toolbarTimeoutObject) {
- clearTimeout(toolbarTimeoutObject);
- toolbarTimeoutObject = null;
- }
- toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
- toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
- }
-
- if (APP.xmpp.isModerator())
- {
-// TODO: Enable settings functionality.
-// Need to uncomment the settings button in index.html.
-// $('#settingsButton').css({visibility:"visible"});
- }
-
- // Show/hide desktop sharing button
- showDesktopSharingButton();
- },
-
-
- /**
- * Docks/undocks the toolbar.
- *
- * @param isDock indicates what operation to perform
- */
- dockToolbar: function (isDock) {
- if (isDock) {
- // First make sure the toolbar is shown.
- if (!$('#header').is(':visible')) {
- this.showToolbar();
- }
-
- // Then clear the time out, to dock the toolbar.
- if (toolbarTimeoutObject) {
- clearTimeout(toolbarTimeoutObject);
- toolbarTimeoutObject = null;
- }
- }
- else {
- if (!$('#header').is(':visible')) {
- this.showToolbar();
- }
- else {
- toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
- }
- }
- },
-
- showDesktopSharingButton: showDesktopSharingButton
-
-};
-
+},{"../../../service/authentication/AuthenticationEvents":94,"../authentication/Authentication":12,"../etherpad/Etherpad":15,"../prezi/Prezi":16,"../side_pannels/SidePanelToggler":18,"../util/MessageHandler":29,"../util/UIUtil":31,"./BottomToolbar":25}],27:[function(require,module,exports){
+/* global $, interfaceConfig, Moderator, DesktopStreaming.showDesktopSharingButton */
+
+var toolbarTimeoutObject,
+ toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
+
+function showDesktopSharingButton() {
+ if (APP.desktopsharing.isDesktopSharingEnabled()) {
+ $('#desktopsharing').css({display: "inline"});
+ } else {
+ $('#desktopsharing').css({display: "none"});
+ }
+}
+
+/**
+ * Hides the toolbar.
+ */
+function hideToolbar() {
+ var header = $("#header"),
+ bottomToolbar = $("#bottomToolbar");
+ var isToolbarHover = false;
+ header.find('*').each(function () {
+ var id = $(this).attr('id');
+ if ($("#" + id + ":hover").length > 0) {
+ isToolbarHover = true;
+ }
+ });
+ if ($("#bottomToolbar:hover").length > 0) {
+ isToolbarHover = true;
+ }
+
+ clearTimeout(toolbarTimeoutObject);
+ toolbarTimeoutObject = null;
+
+ if (!isToolbarHover) {
+ header.hide("slide", { direction: "up", duration: 300});
+ $('#subject').animate({top: "-=40"}, 300);
+ if ($("#remoteVideos").hasClass("hidden")) {
+ bottomToolbar.hide(
+ "slide", {direction: "right", duration: 300});
+ }
+ }
+ else {
+ toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
+ }
+}
+
+var ToolbarToggler = {
+ /**
+ * Shows the main toolbar.
+ */
+ showToolbar: function () {
+ var header = $("#header"),
+ bottomToolbar = $("#bottomToolbar");
+ if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
+ header.show("slide", { direction: "up", duration: 300});
+ $('#subject').animate({top: "+=40"}, 300);
+ if (!bottomToolbar.is(":visible")) {
+ bottomToolbar.show(
+ "slide", {direction: "right", duration: 300});
+ }
+
+ if (toolbarTimeoutObject) {
+ clearTimeout(toolbarTimeoutObject);
+ toolbarTimeoutObject = null;
+ }
+ toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
+ toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
+ }
+
+ if (APP.xmpp.isModerator())
+ {
+// TODO: Enable settings functionality.
+// Need to uncomment the settings button in index.html.
+// $('#settingsButton').css({visibility:"visible"});
+ }
+
+ // Show/hide desktop sharing button
+ showDesktopSharingButton();
+ },
+
+
+ /**
+ * Docks/undocks the toolbar.
+ *
+ * @param isDock indicates what operation to perform
+ */
+ dockToolbar: function (isDock) {
+ if (isDock) {
+ // First make sure the toolbar is shown.
+ if (!$('#header').is(':visible')) {
+ this.showToolbar();
+ }
+
+ // Then clear the time out, to dock the toolbar.
+ if (toolbarTimeoutObject) {
+ clearTimeout(toolbarTimeoutObject);
+ toolbarTimeoutObject = null;
+ }
+ }
+ else {
+ if (!$('#header').is(':visible')) {
+ this.showToolbar();
+ }
+ else {
+ toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
+ }
+ }
+ },
+
+ showDesktopSharingButton: showDesktopSharingButton
+
+};
+
module.exports = ToolbarToggler;
-},{}],27:[function(require,module,exports){
-var JitsiPopover = (function () {
- /**
- * Constructs new JitsiPopover and attaches it to the element
- * @param element jquery selector
- * @param options the options for the popover.
- * @constructor
- */
- function JitsiPopover(element, options)
- {
- this.options = {
- skin: "white",
- content: ""
- };
- if(options)
- {
- if(options.skin)
- this.options.skin = options.skin;
-
- if(options.content)
- this.options.content = options.content;
- }
-
- this.elementIsHovered = false;
- this.popoverIsHovered = false;
- this.popoverShown = false;
-
- element.data("jitsi_popover", this);
- this.element = element;
- this.template = ' ';
- var self = this;
- this.element.on("mouseenter", function () {
- self.elementIsHovered = true;
- self.show();
- }).on("mouseleave", function () {
- self.elementIsHovered = false;
- setTimeout(function () {
- self.hide();
- }, 10);
- });
- }
-
- /**
- * Shows the popover
- */
- JitsiPopover.prototype.show = function () {
- this.createPopover();
- this.popoverShown = true;
-
- };
-
- /**
- * Hides the popover
- */
- JitsiPopover.prototype.hide = function () {
- if(!this.elementIsHovered && !this.popoverIsHovered && this.popoverShown)
- {
- this.forceHide();
- }
- };
-
- /**
- * Hides the popover
- */
- JitsiPopover.prototype.forceHide = function () {
- $(".jitsipopover").remove();
- this.popoverShown = false;
- };
-
- /**
- * Creates the popover html
- */
- JitsiPopover.prototype.createPopover = function () {
- $("body").append(this.template);
- $(".jitsipopover > .jitsipopover-content").html(this.options.content);
- var self = this;
- $(".jitsipopover").on("mouseenter", function () {
- self.popoverIsHovered = true;
- }).on("mouseleave", function () {
- self.popoverIsHovered = false;
- self.hide();
- });
-
- this.refreshPosition();
- };
-
- /**
- * Refreshes the position of the popover
- */
- JitsiPopover.prototype.refreshPosition = function () {
- $(".jitsipopover").position({
- my: "bottom",
- at: "top",
- collision: "fit",
- of: this.element,
- using: function (position, elements) {
- var calcLeft = elements.target.left - elements.element.left + elements.target.width/2;
- $(".jitsipopover").css({top: position.top, left: position.left, display: "table"});
- $(".jitsipopover > .arrow").css({left: calcLeft});
- $(".jitsipopover > .jitsiPopupmenuPadding").css({left: calcLeft - 50});
- }
- });
- };
-
- /**
- * Updates the content of popover.
- * @param content new content
- */
- JitsiPopover.prototype.updateContent = function (content) {
- this.options.content = content;
- if(!this.popoverShown)
- return;
- $(".jitsipopover").remove();
- this.createPopover();
- };
-
- return JitsiPopover;
-
-
-})();
-
-module.exports = JitsiPopover;
},{}],28:[function(require,module,exports){
-/* global $, APP, jQuery, toastr */
-var messageHandler = (function(my) {
-
- /**
- * Shows a message to the user.
- *
- * @param titleString the title of the message
- * @param messageString the text of the message
- */
- my.openMessageDialog = function(titleKey, messageKey) {
- var title = null;
- if(titleKey)
- {
- title = APP.translation.generateTranslatonHTML(titleKey);
- }
- var message = APP.translation.generateTranslatonHTML(messageKey);
- $.prompt(message,
- {
- title: title,
- persistent: false
- }
- );
- };
-
- /**
- * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
- *
- * @param titleString the title of the message
- * @param msgString the text of the message
- * @param persistent boolean value which determines whether the message is persistent or not
- * @param leftButton the fist button's text
- * @param submitFunction function to be called on submit
- * @param loadedFunction function to be called after the prompt is fully loaded
- * @param closeFunction function to be called after the prompt is closed
- * @param focus optional focus selector or button index to be focused after
- * the dialog is opened
- * @param defaultButton index of default button which will be activated when
- * the user press 'enter'. Indexed from 0.
- */
- my.openTwoButtonDialog = function(titleKey, titleString, msgKey, msgString,
- persistent, leftButtonKey, submitFunction, loadedFunction,
- closeFunction, focus, defaultButton)
- {
- var buttons = [];
-
- var leftButton = APP.translation.generateTranslatonHTML(leftButtonKey);
- buttons.push({ title: leftButton, value: true});
-
- var cancelButton
- = APP.translation.generateTranslatonHTML("dialog.Cancel");
- buttons.push({title: cancelButton, value: false});
-
- var message = msgString, title = titleString;
- if (titleKey)
- {
- title = APP.translation.generateTranslatonHTML(titleKey);
- }
- if (msgKey) {
- message = APP.translation.generateTranslatonHTML(msgKey);
- }
- $.prompt(message, {
- title: title,
- persistent: false,
- buttons: buttons,
- defaultButton: defaultButton,
- focus: focus,
- loaded: loadedFunction,
- submit: submitFunction,
- close: closeFunction
- });
- };
-
- /**
- * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
- *
- * @param titleString the title of the message
- * @param msgString the text of the message
- * @param persistent boolean value which determines whether the message is persistent or not
- * @param buttons object with the buttons. The keys must be the name of the button and value is the value
- * that will be passed to submitFunction
- * @param submitFunction function to be called on submit
- * @param loadedFunction function to be called after the prompt is fully loaded
- */
- my.openDialog = function (titleString, msgString, persistent, buttons,
- submitFunction, loadedFunction) {
- var args = {
- title: titleString,
- persistent: persistent,
- buttons: buttons,
- defaultButton: 1,
- loaded: loadedFunction,
- submit: submitFunction
- };
- if (persistent) {
- args.closeText = '';
- }
- return new Impromptu(msgString, args);
- };
-
- /**
- * Closes currently opened dialog.
- */
- my.closeDialog = function () {
- $.prompt.close();
- };
-
- /**
- * Shows a dialog with different states to the user.
- *
- * @param statesObject object containing all the states of the dialog
- */
- my.openDialogWithStates = function (statesObject, options) {
-
- return new Impromptu(statesObject, options);
- };
-
- /**
- * Opens new popup window for given url centered over current
- * window.
- *
- * @param url the URL to be displayed in the popup window
- * @param w the width of the popup window
- * @param h the height of the popup window
- * @param onPopupClosed optional callback function called when popup window
- * has been closed.
- *
- * @returns popup window object if opened successfully or undefined
- * in case we failed to open it(popup blocked)
- */
- my.openCenteredPopup = function (url, w, h, onPopupClosed) {
- var l = window.screenX + (window.innerWidth / 2) - (w / 2);
- var t = window.screenY + (window.innerHeight / 2) - (h / 2);
- var popup = window.open(
- url, '_blank',
- 'top=' + t + ', left=' + l + ', width=' + w + ', height=' + h + '');
- if (popup && onPopupClosed) {
- var pollTimer = window.setInterval(function () {
- if (popup.closed !== false) {
- window.clearInterval(pollTimer);
- onPopupClosed();
- }
- }, 200);
- }
- return popup;
- };
-
- /**
- * Shows a dialog prompting the user to send an error report.
- *
- * @param titleString the title of the message
- * @param msgString the text of the message
- * @param error the error that is being reported
- */
- my.openReportDialog = function(titleKey, msgKey, error) {
- my.openMessageDialog(titleKey, msgKey);
- console.log(error);
- //FIXME send the error to the server
- };
-
- /**
- * Shows an error dialog to the user.
- * @param title the title of the message
- * @param message the text of the messafe
- */
- my.showError = function(titleKey, msgKey) {
-
- if(!titleKey) {
- titleKey = "dialog.oops";
- }
- if(!msgKey)
- {
- msgKey = "dialog.defaultError";
- }
- messageHandler.openMessageDialog(titleKey, msgKey);
- };
-
- my.notify = function(displayName, displayNameKey,
- cls, messageKey, messageArguments) {
- var displayNameSpan = '" + displayName;
- }
- else
- {
- displayNameSpan += "data-i18n='" + displayNameKey +
- "'>" + APP.translation.translateString(displayNameKey);
- }
- displayNameSpan += " ";
- toastr.info(
- displayNameSpan + ' ' +
- '" +
- APP.translation.translateString(messageKey,
- messageArguments) +
- ' ');
- };
-
- return my;
-}(messageHandler || {}));
-
-module.exports = messageHandler;
-
-
-
+var JitsiPopover = (function () {
+ /**
+ * Constructs new JitsiPopover and attaches it to the element
+ * @param element jquery selector
+ * @param options the options for the popover.
+ * @constructor
+ */
+ function JitsiPopover(element, options)
+ {
+ this.options = {
+ skin: "white",
+ content: ""
+ };
+ if(options)
+ {
+ if(options.skin)
+ this.options.skin = options.skin;
+
+ if(options.content)
+ this.options.content = options.content;
+ }
+
+ this.elementIsHovered = false;
+ this.popoverIsHovered = false;
+ this.popoverShown = false;
+
+ element.data("jitsi_popover", this);
+ this.element = element;
+ this.template = ' ';
+ var self = this;
+ this.element.on("mouseenter", function () {
+ self.elementIsHovered = true;
+ self.show();
+ }).on("mouseleave", function () {
+ self.elementIsHovered = false;
+ setTimeout(function () {
+ self.hide();
+ }, 10);
+ });
+ }
+
+ /**
+ * Shows the popover
+ */
+ JitsiPopover.prototype.show = function () {
+ this.createPopover();
+ this.popoverShown = true;
+
+ };
+
+ /**
+ * Hides the popover
+ */
+ JitsiPopover.prototype.hide = function () {
+ if(!this.elementIsHovered && !this.popoverIsHovered && this.popoverShown)
+ {
+ this.forceHide();
+ }
+ };
+
+ /**
+ * Hides the popover
+ */
+ JitsiPopover.prototype.forceHide = function () {
+ $(".jitsipopover").remove();
+ this.popoverShown = false;
+ };
+
+ /**
+ * Creates the popover html
+ */
+ JitsiPopover.prototype.createPopover = function () {
+ $("body").append(this.template);
+ $(".jitsipopover > .jitsipopover-content").html(this.options.content);
+ var self = this;
+ $(".jitsipopover").on("mouseenter", function () {
+ self.popoverIsHovered = true;
+ }).on("mouseleave", function () {
+ self.popoverIsHovered = false;
+ self.hide();
+ });
+
+ this.refreshPosition();
+ };
+
+ /**
+ * Refreshes the position of the popover
+ */
+ JitsiPopover.prototype.refreshPosition = function () {
+ $(".jitsipopover").position({
+ my: "bottom",
+ at: "top",
+ collision: "fit",
+ of: this.element,
+ using: function (position, elements) {
+ var calcLeft = elements.target.left - elements.element.left + elements.target.width/2;
+ $(".jitsipopover").css({top: position.top, left: position.left, display: "table"});
+ $(".jitsipopover > .arrow").css({left: calcLeft});
+ $(".jitsipopover > .jitsiPopupmenuPadding").css({left: calcLeft - 50});
+ }
+ });
+ };
+
+ /**
+ * Updates the content of popover.
+ * @param content new content
+ */
+ JitsiPopover.prototype.updateContent = function (content) {
+ this.options.content = content;
+ if(!this.popoverShown)
+ return;
+ $(".jitsipopover").remove();
+ this.createPopover();
+ };
+
+ return JitsiPopover;
+
+
+})();
+
+module.exports = JitsiPopover;
},{}],29:[function(require,module,exports){
-var UIEvents = require("../../../service/UI/UIEvents");
-
-var nickname = null;
-var eventEmitter = null;
-
-var NickanameHandler = {
- init: function (emitter) {
- eventEmitter = emitter;
- var storedDisplayName = window.localStorage.displayname;
- if (storedDisplayName) {
- nickname = storedDisplayName;
- }
- },
- setNickname: function (newNickname) {
- if (!newNickname || nickname === newNickname)
- return;
-
- nickname = newNickname;
- window.localStorage.displayname = nickname;
- eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newNickname);
- },
- getNickname: function () {
- return nickname;
- },
- addListener: function (type, listener) {
- eventEmitter.on(type, listener);
- }
-};
+/* global $, APP, jQuery, toastr */
+var messageHandler = (function(my) {
+
+ /**
+ * Shows a message to the user.
+ *
+ * @param titleString the title of the message
+ * @param messageString the text of the message
+ */
+ my.openMessageDialog = function(titleKey, messageKey) {
+ var title = null;
+ if(titleKey)
+ {
+ title = APP.translation.generateTranslatonHTML(titleKey);
+ }
+ var message = APP.translation.generateTranslatonHTML(messageKey);
+ $.prompt(message,
+ {
+ title: title,
+ persistent: false
+ }
+ );
+ };
+
+ /**
+ * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
+ *
+ * @param titleString the title of the message
+ * @param msgString the text of the message
+ * @param persistent boolean value which determines whether the message is persistent or not
+ * @param leftButton the fist button's text
+ * @param submitFunction function to be called on submit
+ * @param loadedFunction function to be called after the prompt is fully loaded
+ * @param closeFunction function to be called after the prompt is closed
+ * @param focus optional focus selector or button index to be focused after
+ * the dialog is opened
+ * @param defaultButton index of default button which will be activated when
+ * the user press 'enter'. Indexed from 0.
+ */
+ my.openTwoButtonDialog = function(titleKey, titleString, msgKey, msgString,
+ persistent, leftButtonKey, submitFunction, loadedFunction,
+ closeFunction, focus, defaultButton)
+ {
+ var buttons = [];
+
+ var leftButton = APP.translation.generateTranslatonHTML(leftButtonKey);
+ buttons.push({ title: leftButton, value: true});
+
+ var cancelButton
+ = APP.translation.generateTranslatonHTML("dialog.Cancel");
+ buttons.push({title: cancelButton, value: false});
+
+ var message = msgString, title = titleString;
+ if (titleKey)
+ {
+ title = APP.translation.generateTranslatonHTML(titleKey);
+ }
+ if (msgKey) {
+ message = APP.translation.generateTranslatonHTML(msgKey);
+ }
+ $.prompt(message, {
+ title: title,
+ persistent: false,
+ buttons: buttons,
+ defaultButton: defaultButton,
+ focus: focus,
+ loaded: loadedFunction,
+ submit: submitFunction,
+ close: closeFunction
+ });
+ };
+
+ /**
+ * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
+ *
+ * @param titleString the title of the message
+ * @param msgString the text of the message
+ * @param persistent boolean value which determines whether the message is persistent or not
+ * @param buttons object with the buttons. The keys must be the name of the button and value is the value
+ * that will be passed to submitFunction
+ * @param submitFunction function to be called on submit
+ * @param loadedFunction function to be called after the prompt is fully loaded
+ */
+ my.openDialog = function (titleString, msgString, persistent, buttons,
+ submitFunction, loadedFunction) {
+ var args = {
+ title: titleString,
+ persistent: persistent,
+ buttons: buttons,
+ defaultButton: 1,
+ loaded: loadedFunction,
+ submit: submitFunction
+ };
+ if (persistent) {
+ args.closeText = '';
+ }
+ return new Impromptu(msgString, args);
+ };
+
+ /**
+ * Closes currently opened dialog.
+ */
+ my.closeDialog = function () {
+ $.prompt.close();
+ };
+
+ /**
+ * Shows a dialog with different states to the user.
+ *
+ * @param statesObject object containing all the states of the dialog
+ */
+ my.openDialogWithStates = function (statesObject, options) {
+
+ return new Impromptu(statesObject, options);
+ };
+
+ /**
+ * Opens new popup window for given url centered over current
+ * window.
+ *
+ * @param url the URL to be displayed in the popup window
+ * @param w the width of the popup window
+ * @param h the height of the popup window
+ * @param onPopupClosed optional callback function called when popup window
+ * has been closed.
+ *
+ * @returns popup window object if opened successfully or undefined
+ * in case we failed to open it(popup blocked)
+ */
+ my.openCenteredPopup = function (url, w, h, onPopupClosed) {
+ var l = window.screenX + (window.innerWidth / 2) - (w / 2);
+ var t = window.screenY + (window.innerHeight / 2) - (h / 2);
+ var popup = window.open(
+ url, '_blank',
+ 'top=' + t + ', left=' + l + ', width=' + w + ', height=' + h + '');
+ if (popup && onPopupClosed) {
+ var pollTimer = window.setInterval(function () {
+ if (popup.closed !== false) {
+ window.clearInterval(pollTimer);
+ onPopupClosed();
+ }
+ }, 200);
+ }
+ return popup;
+ };
+
+ /**
+ * Shows a dialog prompting the user to send an error report.
+ *
+ * @param titleString the title of the message
+ * @param msgString the text of the message
+ * @param error the error that is being reported
+ */
+ my.openReportDialog = function(titleKey, msgKey, error) {
+ my.openMessageDialog(titleKey, msgKey);
+ console.log(error);
+ //FIXME send the error to the server
+ };
+
+ /**
+ * Shows an error dialog to the user.
+ * @param title the title of the message
+ * @param message the text of the messafe
+ */
+ my.showError = function(titleKey, msgKey) {
+
+ if(!titleKey) {
+ titleKey = "dialog.oops";
+ }
+ if(!msgKey)
+ {
+ msgKey = "dialog.defaultError";
+ }
+ messageHandler.openMessageDialog(titleKey, msgKey);
+ };
+
+ my.notify = function(displayName, displayNameKey,
+ cls, messageKey, messageArguments) {
+ var displayNameSpan = '" + displayName;
+ }
+ else
+ {
+ displayNameSpan += "data-i18n='" + displayNameKey +
+ "'>" + APP.translation.translateString(displayNameKey);
+ }
+ displayNameSpan += " ";
+ toastr.info(
+ displayNameSpan + ' ' +
+ '" +
+ APP.translation.translateString(messageKey,
+ messageArguments) +
+ ' ');
+ };
+
+ return my;
+}(messageHandler || {}));
+
+module.exports = messageHandler;
+
+
+},{}],30:[function(require,module,exports){
+var UIEvents = require("../../../service/UI/UIEvents");
+
+var nickname = null;
+var eventEmitter = null;
+
+var NickanameHandler = {
+ init: function (emitter) {
+ eventEmitter = emitter;
+ var storedDisplayName = window.localStorage.displayname;
+ if (storedDisplayName) {
+ nickname = storedDisplayName;
+ }
+ },
+ setNickname: function (newNickname) {
+ if (!newNickname || nickname === newNickname)
+ return;
+
+ nickname = newNickname;
+ window.localStorage.displayname = nickname;
+ eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newNickname);
+ },
+ getNickname: function () {
+ return nickname;
+ },
+ addListener: function (type, listener) {
+ eventEmitter.on(type, listener);
+ }
+};
+
module.exports = NickanameHandler;
-},{"../../../service/UI/UIEvents":92}],30:[function(require,module,exports){
-/**
- * Created by hristo on 12/22/14.
- */
-module.exports = {
- /**
- * Returns the available video width.
- */
- getAvailableVideoWidth: function () {
- var PanelToggler = require("../side_pannels/SidePanelToggler");
- var rightPanelWidth
- = PanelToggler.isVisible() ? PanelToggler.getPanelSize()[0] : 0;
-
- return window.innerWidth - rightPanelWidth;
- },
- /**
- * Changes the style class of the element given by id.
- */
- buttonClick: function(id, classname) {
- $(id).toggleClass(classname); // add the class to the clicked element
- },
- /**
- * Returns the text width for the given element.
- *
- * @param el the element
- */
- getTextWidth: function (el) {
- return (el.clientWidth + 1);
- },
-
- /**
- * Returns the text height for the given element.
- *
- * @param el the element
- */
- getTextHeight: function (el) {
- return (el.clientHeight + 1);
- },
-
- /**
- * Plays the sound given by id.
- *
- * @param id the identifier of the audio element.
- */
- playSoundNotification: function (id) {
- document.getElementById(id).play();
- },
-
- /**
- * Escapes the given text.
- */
- escapeHtml: function (unsafeText) {
- return $('
').text(unsafeText).html();
- },
-
- imageToGrayScale: function (canvas) {
- var context = canvas.getContext('2d');
- var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
- var pixels = imgData.data;
-
- for (var i = 0, n = pixels.length; i < n; i += 4) {
- var grayscale
- = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
- pixels[i ] = grayscale; // red
- pixels[i+1] = grayscale; // green
- pixels[i+2] = grayscale; // blue
- // pixels[i+3] is alpha
- }
- // redraw the image in black & white
- context.putImageData(imgData, 0, 0);
- },
-
- setTooltip: function (element, key, position) {
- element.setAttribute("data-i18n", "[data-content]" + key);
- element.setAttribute("data-toggle", "popover");
- element.setAttribute("data-placement", position);
- element.setAttribute("data-html", true);
- element.setAttribute("data-container", "body");
- }
-
-
+},{"../../../service/UI/UIEvents":93}],31:[function(require,module,exports){
+/**
+ * Created by hristo on 12/22/14.
+ */
+module.exports = {
+ /**
+ * Returns the available video width.
+ */
+ getAvailableVideoWidth: function () {
+ var PanelToggler = require("../side_pannels/SidePanelToggler");
+ var rightPanelWidth
+ = PanelToggler.isVisible() ? PanelToggler.getPanelSize()[0] : 0;
+
+ return window.innerWidth - rightPanelWidth;
+ },
+ /**
+ * Changes the style class of the element given by id.
+ */
+ buttonClick: function(id, classname) {
+ $(id).toggleClass(classname); // add the class to the clicked element
+ },
+ /**
+ * Returns the text width for the given element.
+ *
+ * @param el the element
+ */
+ getTextWidth: function (el) {
+ return (el.clientWidth + 1);
+ },
+
+ /**
+ * Returns the text height for the given element.
+ *
+ * @param el the element
+ */
+ getTextHeight: function (el) {
+ return (el.clientHeight + 1);
+ },
+
+ /**
+ * Plays the sound given by id.
+ *
+ * @param id the identifier of the audio element.
+ */
+ playSoundNotification: function (id) {
+ document.getElementById(id).play();
+ },
+
+ /**
+ * Escapes the given text.
+ */
+ escapeHtml: function (unsafeText) {
+ return $('
').text(unsafeText).html();
+ },
+
+ imageToGrayScale: function (canvas) {
+ var context = canvas.getContext('2d');
+ var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+ var pixels = imgData.data;
+
+ for (var i = 0, n = pixels.length; i < n; i += 4) {
+ var grayscale
+ = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
+ pixels[i ] = grayscale; // red
+ pixels[i+1] = grayscale; // green
+ pixels[i+2] = grayscale; // blue
+ // pixels[i+3] is alpha
+ }
+ // redraw the image in black & white
+ context.putImageData(imgData, 0, 0);
+ },
+
+ setTooltip: function (element, key, position) {
+ element.setAttribute("data-i18n", "[data-content]" + key);
+ element.setAttribute("data-toggle", "popover");
+ element.setAttribute("data-placement", position);
+ element.setAttribute("data-html", true);
+ element.setAttribute("data-container", "body");
+ }
+
+
};
-},{"../side_pannels/SidePanelToggler":17}],31:[function(require,module,exports){
-var JitsiPopover = require("../util/JitsiPopover");
-
-/**
- * Constructs new connection indicator.
- * @param videoContainer the video container associated with the indicator.
- * @constructor
- */
-function ConnectionIndicator(videoContainer, jid, VideoLayout)
-{
- this.videoContainer = videoContainer;
- this.bandwidth = null;
- this.packetLoss = null;
- this.bitrate = null;
- this.showMoreValue = false;
- this.resolution = null;
- this.transport = [];
- this.popover = null;
- this.jid = jid;
- this.create();
- this.videoLayout = VideoLayout;
-}
-
-/**
- * Values for the connection quality
- * @type {{98: string,
- * 81: string,
- * 64: string,
- * 47: string,
- * 30: string,
- * 0: string}}
- */
-ConnectionIndicator.connectionQualityValues = {
- 98: "18px", //full
- 81: "15px",//4 bars
- 64: "11px",//3 bars
- 47: "7px",//2 bars
- 30: "3px",//1 bar
- 0: "0px"//empty
-};
-
-ConnectionIndicator.getIP = function(value)
-{
- return value.substring(0, value.lastIndexOf(":"));
-};
-
-ConnectionIndicator.getPort = function(value)
-{
- return value.substring(value.lastIndexOf(":") + 1, value.length);
-};
-
-ConnectionIndicator.getStringFromArray = function (array) {
- var res = "";
- for(var i = 0; i < array.length; i++)
- {
- res += (i === 0? "" : ", ") + array[i];
- }
- return res;
-};
-
-/**
- * Generates the html content.
- * @returns {string} the html content.
- */
-ConnectionIndicator.prototype.generateText = function () {
- var downloadBitrate, uploadBitrate, packetLoss, resolution, i;
-
- var translate = APP.translation.translateString;
-
- if(this.bitrate === null)
- {
- downloadBitrate = "N/A";
- uploadBitrate = "N/A";
- }
- else
- {
- downloadBitrate =
- this.bitrate.download? this.bitrate.download + " Kbps" : "N/A";
- uploadBitrate =
- this.bitrate.upload? this.bitrate.upload + " Kbps" : "N/A";
- }
-
- if(this.packetLoss === null)
- {
- packetLoss = "N/A";
- }
- else
- {
-
- packetLoss = "↓ " +
- (this.packetLoss.download !== null? this.packetLoss.download : "N/A") +
- "% ↑ " +
- (this.packetLoss.upload !== null? this.packetLoss.upload : "N/A") + "%";
- }
-
- var resolutionValue = null;
- if(this.resolution && this.jid != null)
- {
- var keys = Object.keys(this.resolution);
- if(keys.length == 1)
- {
- for(var ssrc in this.resolution)
- {
- resolutionValue = this.resolution[ssrc];
- }
- }
- else if(keys.length > 1)
- {
- var displayedSsrc = APP.simulcast.getReceivingSSRC(this.jid);
- resolutionValue = this.resolution[displayedSsrc];
- }
- }
-
- if(this.jid === null)
- {
- resolution = "";
- if(this.resolution === null || !Object.keys(this.resolution) ||
- Object.keys(this.resolution).length === 0)
- {
- resolution = "N/A";
- }
- else
- for(i in this.resolution)
- {
- resolutionValue = this.resolution[i];
- if(resolutionValue)
- {
- if(resolutionValue.height &&
- resolutionValue.width)
- {
- resolution += (resolution === ""? "" : ", ") +
- resolutionValue.width + "x" +
- resolutionValue.height;
- }
- }
- }
- }
- else if(!resolutionValue ||
- !resolutionValue.height ||
- !resolutionValue.width)
- {
- resolution = "N/A";
- }
- else
- {
- resolution = resolutionValue.width + "x" + resolutionValue.height;
- }
-
- var result = "" +
- "" +
- "" +
- translate("connectionindicator.bitrate") + " " +
- "↓ " +
- downloadBitrate + " ↑ " +
- uploadBitrate + " " +
- " " +
- "" +
- translate("connectionindicator.packetloss") + " " +
- "" + packetLoss + " " +
- " " +
- "" +
- translate("connectionindicator.resolution") + " " +
- "" + resolution + "
";
-
- if(this.videoContainer.id == "localVideoContainer") {
- result += "" +
- translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
- "
";
- }
-
- if(this.showMoreValue)
- {
- var downloadBandwidth, uploadBandwidth, transport;
- if(this.bandwidth === null)
- {
- downloadBandwidth = "N/A";
- uploadBandwidth = "N/A";
- }
- else
- {
- downloadBandwidth = this.bandwidth.download?
- this.bandwidth.download + " Kbps" :
- "N/A";
- uploadBandwidth = this.bandwidth.upload?
- this.bandwidth.upload + " Kbps" :
- "N/A";
- }
-
- if(!this.transport || this.transport.length === 0)
- {
- transport = "" +
- "" +
- translate("connectionindicator.address") + " " +
- " N/A ";
- }
- else
- {
- var data = {remoteIP: [], localIP:[], remotePort:[], localPort:[]};
- for(i = 0; i < this.transport.length; i++)
- {
- var ip = ConnectionIndicator.getIP(this.transport[i].ip);
- var port = ConnectionIndicator.getPort(this.transport[i].ip);
- var localIP =
- ConnectionIndicator.getIP(this.transport[i].localip);
- var localPort =
- ConnectionIndicator.getPort(this.transport[i].localip);
- if(data.remoteIP.indexOf(ip) == -1)
- {
- data.remoteIP.push(ip);
- }
-
- if(data.remotePort.indexOf(port) == -1)
- {
- data.remotePort.push(port);
- }
-
- if(data.localIP.indexOf(localIP) == -1)
- {
- data.localIP.push(localIP);
- }
-
- if(data.localPort.indexOf(localPort) == -1)
- {
- data.localPort.push(localPort);
- }
-
- }
-
- var local_address_key = "connectionindicator.localaddress";
- var remote_address_key = "connectionindicator.remoteaddress";
- var localTransport =
- "" +
- translate(local_address_key, {count: data.localIP.length}) +
- " " +
- ConnectionIndicator.getStringFromArray(data.localIP) +
- " ";
- transport =
- "" +
- translate(remote_address_key,
- {count: data.remoteIP.length}) +
- " " +
- ConnectionIndicator.getStringFromArray(data.remoteIP) +
- " ";
-
- var key_remote = "connectionindicator.remoteport",
- key_local = "connectionindicator.localport";
-
- transport += "" +
- "" +
- "" +
- translate(key_remote, {count: this.transport.length}) +
- " ";
- localTransport += " " +
- "" +
- "" +
- translate(key_local, {count: this.transport.length}) +
- " ";
-
- transport +=
- ConnectionIndicator.getStringFromArray(data.remotePort);
- localTransport +=
- ConnectionIndicator.getStringFromArray(data.localPort);
- transport += " ";
- transport += localTransport + "";
- transport +="" +
- "" +
- translate("connectionindicator.transport") + " " +
- "" + this.transport[0].type + " ";
-
- }
-
- result += "" +
- "" +
- "" +
- "" +
- translate("connectionindicator.bandwidth") + " " +
- " " +
- "↓ " +
- downloadBandwidth +
- " ↑ " +
- uploadBandwidth + " ";
-
- result += transport + "
";
-
- }
-
- return result;
-};
-
-/**
- * Shows or hide the additional information.
- */
-ConnectionIndicator.prototype.showMore = function () {
- this.showMoreValue = !this.showMoreValue;
- this.updatePopoverData();
-};
-
-
-function createIcon(classes)
-{
- var icon = document.createElement("span");
- for(var i in classes)
- {
- icon.classList.add(classes[i]);
- }
- icon.appendChild(
- document.createElement("i")).classList.add("icon-connection");
- return icon;
-}
-
-/**
- * Creates the indicator
- */
-ConnectionIndicator.prototype.create = function () {
- this.connectionIndicatorContainer = document.createElement("div");
- this.connectionIndicatorContainer.className = "connectionindicator";
- this.connectionIndicatorContainer.style.display = "none";
- this.videoContainer.appendChild(this.connectionIndicatorContainer);
- this.popover = new JitsiPopover(
- $("#" + this.videoContainer.id + " > .connectionindicator"),
- {content: "" +
- APP.translation.translateString("connectionindicator.na") + "
",
- skin: "black"});
-
- this.emptyIcon = this.connectionIndicatorContainer.appendChild(
- createIcon(["connection", "connection_empty"]));
- this.fullIcon = this.connectionIndicatorContainer.appendChild(
- createIcon(["connection", "connection_full"]));
-
-};
-
-/**
- * Removes the indicator
- */
-ConnectionIndicator.prototype.remove = function()
-{
- this.connectionIndicatorContainer.remove();
- this.popover.forceHide();
-
-};
-
-/**
- * Updates the data of the indicator
- * @param percent the percent of connection quality
- * @param object the statistics data.
- */
-ConnectionIndicator.prototype.updateConnectionQuality =
-function (percent, object) {
-
- if(percent === null)
- {
- this.connectionIndicatorContainer.style.display = "none";
- this.popover.forceHide();
- return;
- }
- else
- {
- if(this.connectionIndicatorContainer.style.display == "none") {
- this.connectionIndicatorContainer.style.display = "block";
- this.videoLayout.updateMutePosition(this.videoContainer.id);
- }
- }
- this.bandwidth = object.bandwidth;
- this.bitrate = object.bitrate;
- this.packetLoss = object.packetLoss;
- this.transport = object.transport;
- if(object.resolution)
- {
- this.resolution = object.resolution;
- }
- for(var quality in ConnectionIndicator.connectionQualityValues)
- {
- if(percent >= quality)
- {
- this.fullIcon.style.width =
- ConnectionIndicator.connectionQualityValues[quality];
- }
- }
- this.updatePopoverData();
-};
-
-/**
- * Updates the resolution
- * @param resolution the new resolution
- */
-ConnectionIndicator.prototype.updateResolution = function (resolution) {
- this.resolution = resolution;
- this.updatePopoverData();
-};
-
-/**
- * Updates the content of the popover
- */
-ConnectionIndicator.prototype.updatePopoverData = function () {
- this.popover.updateContent(
- "" + this.generateText() + "
");
- APP.translation.translateElement($(".connection_info"));
-};
-
-/**
- * Hides the popover
- */
-ConnectionIndicator.prototype.hide = function () {
- this.popover.forceHide();
-};
-
-/**
- * Hides the indicator
- */
-ConnectionIndicator.prototype.hideIndicator = function () {
- this.connectionIndicatorContainer.style.display = "none";
- if(this.popover)
- this.popover.forceHide();
-};
-
+},{"../side_pannels/SidePanelToggler":18}],32:[function(require,module,exports){
+var JitsiPopover = require("../util/JitsiPopover");
+
+/**
+ * Constructs new connection indicator.
+ * @param videoContainer the video container associated with the indicator.
+ * @constructor
+ */
+function ConnectionIndicator(videoContainer, jid, VideoLayout)
+{
+ this.videoContainer = videoContainer;
+ this.bandwidth = null;
+ this.packetLoss = null;
+ this.bitrate = null;
+ this.showMoreValue = false;
+ this.resolution = null;
+ this.transport = [];
+ this.popover = null;
+ this.jid = jid;
+ this.create();
+ this.videoLayout = VideoLayout;
+}
+
+/**
+ * Values for the connection quality
+ * @type {{98: string,
+ * 81: string,
+ * 64: string,
+ * 47: string,
+ * 30: string,
+ * 0: string}}
+ */
+ConnectionIndicator.connectionQualityValues = {
+ 98: "18px", //full
+ 81: "15px",//4 bars
+ 64: "11px",//3 bars
+ 47: "7px",//2 bars
+ 30: "3px",//1 bar
+ 0: "0px"//empty
+};
+
+ConnectionIndicator.getIP = function(value)
+{
+ return value.substring(0, value.lastIndexOf(":"));
+};
+
+ConnectionIndicator.getPort = function(value)
+{
+ return value.substring(value.lastIndexOf(":") + 1, value.length);
+};
+
+ConnectionIndicator.getStringFromArray = function (array) {
+ var res = "";
+ for(var i = 0; i < array.length; i++)
+ {
+ res += (i === 0? "" : ", ") + array[i];
+ }
+ return res;
+};
+
+/**
+ * Generates the html content.
+ * @returns {string} the html content.
+ */
+ConnectionIndicator.prototype.generateText = function () {
+ var downloadBitrate, uploadBitrate, packetLoss, resolution, i;
+
+ var translate = APP.translation.translateString;
+
+ if(this.bitrate === null)
+ {
+ downloadBitrate = "N/A";
+ uploadBitrate = "N/A";
+ }
+ else
+ {
+ downloadBitrate =
+ this.bitrate.download? this.bitrate.download + " Kbps" : "N/A";
+ uploadBitrate =
+ this.bitrate.upload? this.bitrate.upload + " Kbps" : "N/A";
+ }
+
+ if(this.packetLoss === null)
+ {
+ packetLoss = "N/A";
+ }
+ else
+ {
+
+ packetLoss = "↓ " +
+ (this.packetLoss.download !== null? this.packetLoss.download : "N/A") +
+ "% ↑ " +
+ (this.packetLoss.upload !== null? this.packetLoss.upload : "N/A") + "%";
+ }
+
+ var resolutionValue = null;
+ if(this.resolution && this.jid != null)
+ {
+ var keys = Object.keys(this.resolution);
+ if(keys.length == 1)
+ {
+ for(var ssrc in this.resolution)
+ {
+ resolutionValue = this.resolution[ssrc];
+ }
+ }
+ else if(keys.length > 1)
+ {
+ var displayedSsrc = APP.simulcast.getReceivingSSRC(this.jid);
+ resolutionValue = this.resolution[displayedSsrc];
+ }
+ }
+
+ if(this.jid === null)
+ {
+ resolution = "";
+ if(this.resolution === null || !Object.keys(this.resolution) ||
+ Object.keys(this.resolution).length === 0)
+ {
+ resolution = "N/A";
+ }
+ else
+ for(i in this.resolution)
+ {
+ resolutionValue = this.resolution[i];
+ if(resolutionValue)
+ {
+ if(resolutionValue.height &&
+ resolutionValue.width)
+ {
+ resolution += (resolution === ""? "" : ", ") +
+ resolutionValue.width + "x" +
+ resolutionValue.height;
+ }
+ }
+ }
+ }
+ else if(!resolutionValue ||
+ !resolutionValue.height ||
+ !resolutionValue.width)
+ {
+ resolution = "N/A";
+ }
+ else
+ {
+ resolution = resolutionValue.width + "x" + resolutionValue.height;
+ }
+
+ var result = "" +
+ "" +
+ "" +
+ translate("connectionindicator.bitrate") + " " +
+ "↓ " +
+ downloadBitrate + " ↑ " +
+ uploadBitrate + " " +
+ " " +
+ "" +
+ translate("connectionindicator.packetloss") + " " +
+ "" + packetLoss + " " +
+ " " +
+ "" +
+ translate("connectionindicator.resolution") + " " +
+ "" + resolution + "
";
+
+ if(this.videoContainer.id == "localVideoContainer") {
+ result += "" +
+ translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
+ "
";
+ }
+
+ if(this.showMoreValue)
+ {
+ var downloadBandwidth, uploadBandwidth, transport;
+ if(this.bandwidth === null)
+ {
+ downloadBandwidth = "N/A";
+ uploadBandwidth = "N/A";
+ }
+ else
+ {
+ downloadBandwidth = this.bandwidth.download?
+ this.bandwidth.download + " Kbps" :
+ "N/A";
+ uploadBandwidth = this.bandwidth.upload?
+ this.bandwidth.upload + " Kbps" :
+ "N/A";
+ }
+
+ if(!this.transport || this.transport.length === 0)
+ {
+ transport = "" +
+ "" +
+ translate("connectionindicator.address") + " " +
+ " N/A ";
+ }
+ else
+ {
+ var data = {remoteIP: [], localIP:[], remotePort:[], localPort:[]};
+ for(i = 0; i < this.transport.length; i++)
+ {
+ var ip = ConnectionIndicator.getIP(this.transport[i].ip);
+ var port = ConnectionIndicator.getPort(this.transport[i].ip);
+ var localIP =
+ ConnectionIndicator.getIP(this.transport[i].localip);
+ var localPort =
+ ConnectionIndicator.getPort(this.transport[i].localip);
+ if(data.remoteIP.indexOf(ip) == -1)
+ {
+ data.remoteIP.push(ip);
+ }
+
+ if(data.remotePort.indexOf(port) == -1)
+ {
+ data.remotePort.push(port);
+ }
+
+ if(data.localIP.indexOf(localIP) == -1)
+ {
+ data.localIP.push(localIP);
+ }
+
+ if(data.localPort.indexOf(localPort) == -1)
+ {
+ data.localPort.push(localPort);
+ }
+
+ }
+
+ var local_address_key = "connectionindicator.localaddress";
+ var remote_address_key = "connectionindicator.remoteaddress";
+ var localTransport =
+ "" +
+ translate(local_address_key, {count: data.localIP.length}) +
+ " " +
+ ConnectionIndicator.getStringFromArray(data.localIP) +
+ " ";
+ transport =
+ "" +
+ translate(remote_address_key,
+ {count: data.remoteIP.length}) +
+ " " +
+ ConnectionIndicator.getStringFromArray(data.remoteIP) +
+ " ";
+
+ var key_remote = "connectionindicator.remoteport",
+ key_local = "connectionindicator.localport";
+
+ transport += "" +
+ "" +
+ "" +
+ translate(key_remote, {count: this.transport.length}) +
+ " ";
+ localTransport += " " +
+ "" +
+ "" +
+ translate(key_local, {count: this.transport.length}) +
+ " ";
+
+ transport +=
+ ConnectionIndicator.getStringFromArray(data.remotePort);
+ localTransport +=
+ ConnectionIndicator.getStringFromArray(data.localPort);
+ transport += " ";
+ transport += localTransport + "";
+ transport +="" +
+ "" +
+ translate("connectionindicator.transport") + " " +
+ "" + this.transport[0].type + " ";
+
+ }
+
+ result += "" +
+ "" +
+ "" +
+ "" +
+ translate("connectionindicator.bandwidth") + " " +
+ " " +
+ "↓ " +
+ downloadBandwidth +
+ " ↑ " +
+ uploadBandwidth + " ";
+
+ result += transport + "
";
+
+ }
+
+ return result;
+};
+
+/**
+ * Shows or hide the additional information.
+ */
+ConnectionIndicator.prototype.showMore = function () {
+ this.showMoreValue = !this.showMoreValue;
+ this.updatePopoverData();
+};
+
+
+function createIcon(classes)
+{
+ var icon = document.createElement("span");
+ for(var i in classes)
+ {
+ icon.classList.add(classes[i]);
+ }
+ icon.appendChild(
+ document.createElement("i")).classList.add("icon-connection");
+ return icon;
+}
+
+/**
+ * Creates the indicator
+ */
+ConnectionIndicator.prototype.create = function () {
+ this.connectionIndicatorContainer = document.createElement("div");
+ this.connectionIndicatorContainer.className = "connectionindicator";
+ this.connectionIndicatorContainer.style.display = "none";
+ this.videoContainer.appendChild(this.connectionIndicatorContainer);
+ this.popover = new JitsiPopover(
+ $("#" + this.videoContainer.id + " > .connectionindicator"),
+ {content: "" +
+ APP.translation.translateString("connectionindicator.na") + "
",
+ skin: "black"});
+
+ this.emptyIcon = this.connectionIndicatorContainer.appendChild(
+ createIcon(["connection", "connection_empty"]));
+ this.fullIcon = this.connectionIndicatorContainer.appendChild(
+ createIcon(["connection", "connection_full"]));
+
+};
+
+/**
+ * Removes the indicator
+ */
+ConnectionIndicator.prototype.remove = function()
+{
+ this.connectionIndicatorContainer.remove();
+ this.popover.forceHide();
+
+};
+
+/**
+ * Updates the data of the indicator
+ * @param percent the percent of connection quality
+ * @param object the statistics data.
+ */
+ConnectionIndicator.prototype.updateConnectionQuality =
+function (percent, object) {
+
+ if(percent === null)
+ {
+ this.connectionIndicatorContainer.style.display = "none";
+ this.popover.forceHide();
+ return;
+ }
+ else
+ {
+ if(this.connectionIndicatorContainer.style.display == "none") {
+ this.connectionIndicatorContainer.style.display = "block";
+ this.videoLayout.updateMutePosition(this.videoContainer.id);
+ }
+ }
+ this.bandwidth = object.bandwidth;
+ this.bitrate = object.bitrate;
+ this.packetLoss = object.packetLoss;
+ this.transport = object.transport;
+ if(object.resolution)
+ {
+ this.resolution = object.resolution;
+ }
+ for(var quality in ConnectionIndicator.connectionQualityValues)
+ {
+ if(percent >= quality)
+ {
+ this.fullIcon.style.width =
+ ConnectionIndicator.connectionQualityValues[quality];
+ }
+ }
+ this.updatePopoverData();
+};
+
+/**
+ * Updates the resolution
+ * @param resolution the new resolution
+ */
+ConnectionIndicator.prototype.updateResolution = function (resolution) {
+ this.resolution = resolution;
+ this.updatePopoverData();
+};
+
+/**
+ * Updates the content of the popover
+ */
+ConnectionIndicator.prototype.updatePopoverData = function () {
+ this.popover.updateContent(
+ "" + this.generateText() + "
");
+ APP.translation.translateElement($(".connection_info"));
+};
+
+/**
+ * Hides the popover
+ */
+ConnectionIndicator.prototype.hide = function () {
+ this.popover.forceHide();
+};
+
+/**
+ * Hides the indicator
+ */
+ConnectionIndicator.prototype.hideIndicator = function () {
+ this.connectionIndicatorContainer.style.display = "none";
+ if(this.popover)
+ this.popover.forceHide();
+};
+
module.exports = ConnectionIndicator;
-},{"../util/JitsiPopover":27}],32:[function(require,module,exports){
-var AudioLevels = require("../audio_levels/AudioLevels");
-var Avatar = require("../avatar/Avatar");
-var Chat = require("../side_pannels/chat/Chat");
-var ContactList = require("../side_pannels/contactlist/ContactList");
-var UIUtil = require("../util/UIUtil");
-var ConnectionIndicator = require("./ConnectionIndicator");
-var NicknameHandler = require("../util/NicknameHandler");
-var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
-var UIEvents = require("../../../service/UI/UIEvents");
-
-var currentDominantSpeaker = null;
-var lastNCount = config.channelLastN;
-var localLastNCount = config.channelLastN;
-var localLastNSet = [];
-var lastNEndpointsCache = [];
-var lastNPickupJid = null;
-var largeVideoState = {
- updateInProgress: false,
- newSrc: ''
-};
-
-var eventEmitter = null;
-
-/**
- * Currently focused video "src"(displayed in large video).
- * @type {String}
- */
-var focusedVideoInfo = null;
-
-/**
- * Indicates if we have muted our audio before the conference has started.
- * @type {boolean}
- */
-var preMuted = false;
-
-var mutedAudios = {};
-
-var flipXLocalVideo = true;
-var currentVideoWidth = null;
-var currentVideoHeight = null;
-
-var localVideoSrc = null;
-
-function videoactive( videoelem) {
- if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
- // ignore mixedmslabela0 and v0
-
- videoelem.show();
- VideoLayout.resizeThumbnails();
-
- var videoParent = videoelem.parent();
- var parentResourceJid = null;
- if (videoParent)
- parentResourceJid
- = VideoLayout.getPeerContainerResourceJid(videoParent[0]);
-
- // Update the large video to the last added video only if there's no
- // current dominant, focused speaker or prezi playing or update it to
- // the current dominant speaker.
- if ((!focusedVideoInfo &&
- !VideoLayout.getDominantSpeakerResourceJid() &&
- !require("../prezi/Prezi").isPresentationVisible()) ||
- (parentResourceJid &&
- VideoLayout.getDominantSpeakerResourceJid() === parentResourceJid)) {
- VideoLayout.updateLargeVideo(
- APP.RTC.getVideoSrc(videoelem[0]),
- 1,
- parentResourceJid);
- }
-
- VideoLayout.showModeratorIndicator();
- }
-}
-
-function waitForRemoteVideo(selector, ssrc, stream, jid) {
- // XXX(gp) so, every call to this function is *always* preceded by a call
- // to the RTC.attachMediaStream() function but that call is *not* followed
- // by an update to the videoSrcToSsrc map!
- //
- // The above way of doing things results in video SRCs that don't correspond
- // to any SSRC for a short period of time (to be more precise, for as long
- // the waitForRemoteVideo takes to complete). This causes problems (see
- // bellow).
- //
- // I'm wondering why we need to do that; i.e. why call RTC.attachMediaStream()
- // a second time in here and only then update the videoSrcToSsrc map? Why
- // not simply update the videoSrcToSsrc map when the RTC.attachMediaStream()
- // is called the first time? I actually do that in the lastN changed event
- // handler because the "orphan" video SRC is causing troubles there. The
- // purpose of this method would then be to fire the "videoactive.jingle".
- //
- // Food for though I guess :-)
-
- if (selector.removed || !selector.parent().is(":visible")) {
- console.warn("Media removed before had started", selector);
- return;
- }
-
- if (stream.id === 'mixedmslabel') return;
-
- if (selector[0].currentTime > 0) {
- var videoStream = APP.simulcast.getReceivingVideoStream(stream);
- APP.RTC.attachMediaStream(selector, videoStream); // FIXME: why do i have to do this for FF?
- videoactive(selector);
- } else {
- setTimeout(function () {
- waitForRemoteVideo(selector, ssrc, stream, jid);
- }, 250);
- }
-}
-
-/**
- * Returns an array of the video horizontal and vertical indents,
- * so that if fits its parent.
- *
- * @return an array with 2 elements, the horizontal indent and the vertical
- * indent
- */
-function getCameraVideoPosition(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight) {
- // Parent height isn't completely calculated when we position the video in
- // full screen mode and this is why we use the screen height in this case.
- // Need to think it further at some point and implement it properly.
- var isFullScreen = document.fullScreen ||
- document.mozFullScreen ||
- document.webkitIsFullScreen;
- if (isFullScreen)
- videoSpaceHeight = window.innerHeight;
-
- var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
- var verticalIndent = (videoSpaceHeight - videoHeight) / 2;
-
- return [horizontalIndent, verticalIndent];
-}
-
-/**
- * Returns an array of the video horizontal and vertical indents.
- * Centers horizontally and top aligns vertically.
- *
- * @return an array with 2 elements, the horizontal indent and the vertical
- * indent
- */
-function getDesktopVideoPosition(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight) {
-
- var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
-
- var verticalIndent = 0;// Top aligned
-
- return [horizontalIndent, verticalIndent];
-}
-
-
-/**
- * Returns an array of the video dimensions, so that it covers the screen.
- * It leaves no empty areas, but some parts of the video might not be visible.
- *
- * @return an array with 2 elements, the video width and the video height
- */
-function getCameraVideoSize(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight) {
- if (!videoWidth)
- videoWidth = currentVideoWidth;
- if (!videoHeight)
- videoHeight = currentVideoHeight;
-
- var aspectRatio = videoWidth / videoHeight;
-
- var availableWidth = Math.max(videoWidth, videoSpaceWidth);
- var availableHeight = Math.max(videoHeight, videoSpaceHeight);
-
- if (availableWidth / aspectRatio < videoSpaceHeight) {
- availableHeight = videoSpaceHeight;
- availableWidth = availableHeight * aspectRatio;
- }
-
- if (availableHeight * aspectRatio < videoSpaceWidth) {
- availableWidth = videoSpaceWidth;
- availableHeight = availableWidth / aspectRatio;
- }
-
- return [availableWidth, availableHeight];
-}
-
-/**
- * Sets the display name for the given video span id.
- */
-function setDisplayName(videoSpanId, displayName, key) {
- var nameSpan = $('#' + videoSpanId + '>span.displayname');
- var defaultLocalDisplayName = APP.translation.generateTranslatonHTML(
- interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
-
- // If we already have a display name for this video.
- if (nameSpan.length > 0) {
- var nameSpanElement = nameSpan.get(0);
-
- if (nameSpanElement.id === 'localDisplayName' &&
- $('#localDisplayName').text() !== displayName) {
- if (displayName && displayName.length > 0)
- {
- var meHTML = APP.translation.generateTranslatonHTML("me");
- $('#localDisplayName').html(displayName + ' (' + meHTML + ')');
- }
- else
- $('#localDisplayName').html(defaultLocalDisplayName);
- } else {
- if (displayName && displayName.length > 0)
- {
- $('#' + videoSpanId + '_name').html(displayName);
- }
- else if (key && key.length > 0)
- {
- var nameHtml = APP.translation.generateTranslatonHTML(key);
- $('#' + videoSpanId + '_name').html(nameHtml);
- }
- else
- $('#' + videoSpanId + '_name').text(
- interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
- }
- } else {
- var editButton = null;
-
- nameSpan = document.createElement('span');
- nameSpan.className = 'displayname';
- $('#' + videoSpanId)[0].appendChild(nameSpan);
-
- if (videoSpanId === 'localVideoContainer') {
- editButton = createEditDisplayNameButton();
- if (displayName && displayName.length > 0) {
- var meHTML = APP.translation.generateTranslatonHTML("me");
- nameSpan.innerHTML = displayName + meHTML;
- }
- else
- nameSpan.innerHTML = defaultLocalDisplayName;
- }
- else {
- if (displayName && displayName.length > 0) {
-
- nameSpan.innerText = displayName;
- }
- else
- nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
- }
-
-
- if (!editButton) {
- nameSpan.id = videoSpanId + '_name';
- } else {
- nameSpan.id = 'localDisplayName';
- $('#' + videoSpanId)[0].appendChild(editButton);
- //translates popover of edit button
- APP.translation.translateElement($("a.displayname"));
-
- var editableText = document.createElement('input');
- editableText.className = 'displayname';
- editableText.type = 'text';
- editableText.id = 'editDisplayName';
-
- if (displayName && displayName.length) {
- editableText.value
- = displayName;
- }
-
- var defaultNickname = APP.translation.translateString(
- "defaultNickname", {name: "Jane Pink"});
- editableText.setAttribute('style', 'display:none;');
- editableText.setAttribute('data-18n',
- '[placeholder]defaultNickname');
- editableText.setAttribute("data-i18n-options",
- JSON.stringify({name: "Jane Pink"}));
- editableText.setAttribute("placeholder", defaultNickname);
-
- $('#' + videoSpanId)[0].appendChild(editableText);
-
- $('#localVideoContainer .displayname')
- .bind("click", function (e) {
-
- e.preventDefault();
- e.stopPropagation();
- $('#localDisplayName').hide();
- $('#editDisplayName').show();
- $('#editDisplayName').focus();
- $('#editDisplayName').select();
-
- $('#editDisplayName').one("focusout", function (e) {
- VideoLayout.inputDisplayNameHandler(this.value);
- });
-
- $('#editDisplayName').on('keydown', function (e) {
- if (e.keyCode === 13) {
- e.preventDefault();
- VideoLayout.inputDisplayNameHandler(this.value);
- }
- });
- });
- }
- }
-}
-
-/**
- * Gets the selector of video thumbnail container for the user identified by
- * given userJid
- * @param resourceJid user's Jid for whom we want to get the video container.
- */
-function getParticipantContainer(resourceJid)
-{
- if (!resourceJid)
- return null;
-
- if (resourceJid === APP.xmpp.myResource())
- return $("#localVideoContainer");
- else
- return $("#participant_" + resourceJid);
-}
-
-/**
- * Sets the size and position of the given video element.
- *
- * @param video the video element to position
- * @param width the desired video width
- * @param height the desired video height
- * @param horizontalIndent the left and right indent
- * @param verticalIndent the top and bottom indent
- */
-function positionVideo(video,
- width,
- height,
- horizontalIndent,
- verticalIndent) {
- video.width(width);
- video.height(height);
- video.css({ top: verticalIndent + 'px',
- bottom: verticalIndent + 'px',
- left: horizontalIndent + 'px',
- right: horizontalIndent + 'px'});
-}
-
-/**
- * Adds the remote video menu element for the given jid in the
- * given parentElement .
- *
- * @param jid the jid indicating the video for which we're adding a menu.
- * @param parentElement the parent element where this menu will be added
- */
-function addRemoteVideoMenu(jid, parentElement) {
- var spanElement = document.createElement('span');
- spanElement.className = 'remotevideomenu';
-
- parentElement.appendChild(spanElement);
-
- var menuElement = document.createElement('i');
- menuElement.className = 'fa fa-angle-down';
- menuElement.title = 'Remote user controls';
- spanElement.appendChild(menuElement);
-
-//
-
- var popupmenuElement = document.createElement('ul');
- popupmenuElement.className = 'popupmenu';
- popupmenuElement.id
- = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
- spanElement.appendChild(popupmenuElement);
-
- var muteMenuItem = document.createElement('li');
- var muteLinkItem = document.createElement('a');
-
- var mutedIndicator = " ";
-
- if (!mutedAudios[jid]) {
- muteLinkItem.innerHTML = mutedIndicator +
- "
";
- muteLinkItem.className = 'mutelink';
- }
- else {
- muteLinkItem.innerHTML = mutedIndicator +
- "
";
- muteLinkItem.className = 'mutelink disabled';
- }
-
- muteLinkItem.onclick = function(){
- if ($(this).attr('disabled') != undefined) {
- event.preventDefault();
- }
- var isMute = mutedAudios[jid] == true;
- APP.xmpp.setMute(jid, !isMute);
-
- popupmenuElement.setAttribute('style', 'display:none;');
-
- if (isMute) {
- this.innerHTML = mutedIndicator +
- "
";
- this.className = 'mutelink disabled';
- }
- else {
- this.innerHTML = mutedIndicator +
- "
";
- this.className = 'mutelink';
- }
- };
-
- muteMenuItem.appendChild(muteLinkItem);
- popupmenuElement.appendChild(muteMenuItem);
-
- var ejectIndicator = " ";
-
- var ejectMenuItem = document.createElement('li');
- var ejectLinkItem = document.createElement('a');
- var ejectText = "
";
- ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
- ejectLinkItem.onclick = function(){
- APP.xmpp.eject(jid);
- popupmenuElement.setAttribute('style', 'display:none;');
- };
-
- ejectMenuItem.appendChild(ejectLinkItem);
- popupmenuElement.appendChild(ejectMenuItem);
-
- var paddingSpan = document.createElement('span');
- paddingSpan.className = 'popupmenuPadding';
- popupmenuElement.appendChild(paddingSpan);
- APP.translation.translateElement($("#" + popupmenuElement.id + " > li > a > div"));
-}
-
-/**
- * Removes remote video menu element from video element identified by
- * given videoElementId .
- *
- * @param videoElementId the id of local or remote video element.
- */
-function removeRemoteVideoMenu(videoElementId) {
- var menuSpan = $('#' + videoElementId + '>span.remotevideomenu');
- if (menuSpan.length) {
- menuSpan.remove();
- }
-}
-
-/**
- * Updates the data for the indicator
- * @param id the id of the indicator
- * @param percent the percent for connection quality
- * @param object the data
- */
-function updateStatsIndicator(id, percent, object) {
- if(VideoLayout.connectionIndicators[id])
- VideoLayout.connectionIndicators[id].updateConnectionQuality(percent, object);
-}
-
-
-/**
- * Returns an array of the video dimensions, so that it keeps it's aspect
- * ratio and fits available area with it's larger dimension. This method
- * ensures that whole video will be visible and can leave empty areas.
- *
- * @return an array with 2 elements, the video width and the video height
- */
-function getDesktopVideoSize(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight) {
- if (!videoWidth)
- videoWidth = currentVideoWidth;
- if (!videoHeight)
- videoHeight = currentVideoHeight;
-
- var aspectRatio = videoWidth / videoHeight;
-
- var availableWidth = Math.max(videoWidth, videoSpaceWidth);
- var availableHeight = Math.max(videoHeight, videoSpaceHeight);
-
- videoSpaceHeight -= $('#remoteVideos').outerHeight();
-
- if (availableWidth / aspectRatio >= videoSpaceHeight)
- {
- availableHeight = videoSpaceHeight;
- availableWidth = availableHeight * aspectRatio;
- }
-
- if (availableHeight * aspectRatio >= videoSpaceWidth)
- {
- availableWidth = videoSpaceWidth;
- availableHeight = availableWidth / aspectRatio;
- }
-
- return [availableWidth, availableHeight];
-}
-
-/**
- * Creates the edit display name button.
- *
- * @returns the edit button
- */
-function createEditDisplayNameButton() {
- var editButton = document.createElement('a');
- editButton.className = 'displayname';
- UIUtil.setTooltip(editButton,
- "videothumbnail.editnickname",
- "top");
- editButton.innerHTML = ' ';
-
- return editButton;
-}
-
-/**
- * Creates the element indicating the moderator(owner) of the conference.
- *
- * @param parentElement the parent element where the owner indicator will
- * be added
- */
-function createModeratorIndicatorElement(parentElement) {
- var moderatorIndicator = document.createElement('i');
- moderatorIndicator.className = 'fa fa-star';
- parentElement.appendChild(moderatorIndicator);
-
- UIUtil.setTooltip(parentElement,
- "videothumbnail.moderator",
- "top");
-}
-
-
-var VideoLayout = (function (my) {
- my.connectionIndicators = {};
-
- // By default we use camera
- my.getVideoSize = getCameraVideoSize;
- my.getVideoPosition = getCameraVideoPosition;
-
- my.init = function (emitter) {
- // Listen for large video size updates
- document.getElementById('largeVideo')
- .addEventListener('loadedmetadata', function (e) {
- currentVideoWidth = this.videoWidth;
- currentVideoHeight = this.videoHeight;
- VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight);
- });
- eventEmitter = emitter;
- };
-
- my.isInLastN = function(resource) {
- return lastNCount < 0 // lastN is disabled, return true
- || (lastNCount > 0 && lastNEndpointsCache.length == 0) // lastNEndpoints cache not built yet, return true
- || (lastNEndpointsCache && lastNEndpointsCache.indexOf(resource) !== -1);
- };
-
- my.changeLocalStream = function (stream) {
- VideoLayout.changeLocalVideo(stream);
- };
-
- my.changeLocalAudio = function(stream) {
- APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
- document.getElementById('localAudio').autoplay = true;
- document.getElementById('localAudio').volume = 0;
- if (preMuted) {
- if(!APP.UI.setAudioMuted(true))
- {
- preMuted = mute;
- }
- preMuted = false;
- }
- };
-
- my.changeLocalVideo = function(stream) {
- var flipX = true;
- if(stream.videoType == "screen")
- flipX = false;
- var localVideo = document.createElement('video');
- localVideo.id = 'localVideo_' +
- APP.RTC.getStreamID(stream.getOriginalStream());
- localVideo.autoplay = true;
- localVideo.volume = 0; // is it required if audio is separated ?
- localVideo.oncontextmenu = function () { return false; };
-
- var localVideoContainer = document.getElementById('localVideoWrapper');
- localVideoContainer.appendChild(localVideo);
-
- // Set default display name.
- setDisplayName('localVideoContainer');
-
- if(!VideoLayout.connectionIndicators["localVideoContainer"]) {
- VideoLayout.connectionIndicators["localVideoContainer"]
- = new ConnectionIndicator($("#localVideoContainer")[0], null, VideoLayout);
- }
-
- AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
-
- var localVideoSelector = $('#' + localVideo.id);
-
- function localVideoClick(event) {
- event.stopPropagation();
- VideoLayout.handleVideoThumbClicked(
- APP.RTC.getVideoSrc(localVideo),
- false,
- APP.xmpp.myResource());
- }
- // Add click handler to both video and video wrapper elements in case
- // there's no video.
- localVideoSelector.click(localVideoClick);
- $('#localVideoContainer').click(localVideoClick);
-
- // Add hover handler
- $('#localVideoContainer').hover(
- function() {
- VideoLayout.showDisplayName('localVideoContainer', true);
- },
- function() {
- if (!VideoLayout.isLargeVideoVisible()
- || APP.RTC.getVideoSrc(localVideo) !== APP.RTC.getVideoSrc($('#largeVideo')[0]))
- VideoLayout.showDisplayName('localVideoContainer', false);
- }
- );
- // Add stream ended handler
- stream.getOriginalStream().onended = function () {
- localVideoContainer.removeChild(localVideo);
- VideoLayout.updateRemovedVideo(APP.RTC.getVideoSrc(localVideo));
- };
- // Flip video x axis if needed
- flipXLocalVideo = flipX;
- if (flipX) {
- localVideoSelector.addClass("flipVideoX");
- }
- // Attach WebRTC stream
- var videoStream = APP.simulcast.getLocalVideoStream();
- APP.RTC.attachMediaStream(localVideoSelector, videoStream);
-
- localVideoSrc = APP.RTC.getVideoSrc(localVideo);
-
- var myResourceJid = APP.xmpp.myResource();
-
- VideoLayout.updateLargeVideo(localVideoSrc, 0,
- myResourceJid);
-
- };
-
- /**
- * Checks if removed video is currently displayed and tries to display
- * another one instead.
- * @param removedVideoSrc src stream identifier of the video.
- */
- my.updateRemovedVideo = function(removedVideoSrc) {
- if (removedVideoSrc === APP.RTC.getVideoSrc($('#largeVideo')[0])) {
- // this is currently displayed as large
- // pick the last visible video in the row
- // if nobody else is left, this picks the local video
- var pick
- = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
- .get(0);
-
- if (!pick) {
- console.info("Last visible video no longer exists");
- pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
-
- if (!pick || !APP.RTC.getVideoSrc(pick)) {
- // Try local video
- console.info("Fallback to local video...");
- pick = $('#remoteVideos>span>span>video').get(0);
- }
- }
-
- // mute if localvideo
- if (pick) {
- var container = pick.parentNode;
- var jid = null;
- if(container)
- {
- if(container.id == "localVideoWrapper")
- {
- jid = APP.xmpp.myResource();
- }
- else
- {
- jid = VideoLayout.getPeerContainerResourceJid(container);
- }
- }
-
- VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(pick), pick.volume, jid);
- } else {
- console.warn("Failed to elect large video");
- }
- }
- };
-
- my.onRemoteStreamAdded = function (stream) {
- var container;
- var remotes = document.getElementById('remoteVideos');
-
- if (stream.peerjid) {
- VideoLayout.ensurePeerContainerExists(stream.peerjid);
-
- container = document.getElementById(
- 'participant_' + Strophe.getResourceFromJid(stream.peerjid));
- } else {
- var id = stream.getOriginalStream().id;
- if (id !== 'mixedmslabel'
- // FIXME: default stream is added always with new focus
- // (to be investigated)
- && id !== 'default') {
- console.error('can not associate stream',
- id,
- 'with a participant');
- // We don't want to add it here since it will cause troubles
- return;
- }
- // FIXME: for the mixed ms we dont need a video -- currently
- container = document.createElement('span');
- container.id = 'mixedstream';
- container.className = 'videocontainer';
- remotes.appendChild(container);
- UIUtil.playSoundNotification('userJoined');
- }
-
- if (container) {
- VideoLayout.addRemoteStreamElement( container,
- stream.sid,
- stream.getOriginalStream(),
- stream.peerjid,
- stream.ssrc);
- }
- }
-
- my.getLargeVideoState = function () {
- return largeVideoState;
- };
-
- /**
- * Updates the large video with the given new video source.
- */
- my.updateLargeVideo = function(newSrc, vol, resourceJid) {
- console.log('hover in', newSrc);
-
- if (APP.RTC.getVideoSrc($('#largeVideo')[0]) !== newSrc) {
-
- $('#activeSpeaker').css('visibility', 'hidden');
- // Due to the simulcast the localVideoSrc may have changed when the
- // fadeOut event triggers. In that case the getJidFromVideoSrc and
- // isVideoSrcDesktop methods will not function correctly.
- //
- // Also, again due to the simulcast, the updateLargeVideo method can
- // be called multiple times almost simultaneously. Therefore, we
- // store the state here and update only once.
-
- largeVideoState.newSrc = newSrc;
- largeVideoState.isVisible = $('#largeVideo').is(':visible');
- largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop(
- APP.xmpp.findJidFromResource(resourceJid));
-
- if(largeVideoState.userResourceJid) {
- largeVideoState.oldResourceJid = largeVideoState.userResourceJid;
- } else {
- largeVideoState.oldResourceJid = null;
- }
- largeVideoState.userResourceJid = resourceJid;
-
- // Screen stream is already rotated
- largeVideoState.flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
-
- var userChanged = false;
- if (largeVideoState.oldResourceJid !== largeVideoState.userResourceJid) {
- userChanged = true;
- // we want the notification to trigger even if userJid is undefined,
- // or null.
- eventEmitter.emit(UIEvents.SELECTED_ENDPOINT,
- largeVideoState.userResourceJid);
- }
-
- if (!largeVideoState.updateInProgress) {
- largeVideoState.updateInProgress = true;
-
- var doUpdate = function () {
-
- Avatar.updateActiveSpeakerAvatarSrc(
- APP.xmpp.findJidFromResource(
- largeVideoState.userResourceJid));
-
- if (!userChanged && largeVideoState.preload &&
- largeVideoState.preload !== null &&
- APP.RTC.getVideoSrc($(largeVideoState.preload)[0]) === newSrc)
- {
-
- console.info('Switching to preloaded video');
- var attributes = $('#largeVideo').prop("attributes");
-
- // loop through largeVideo attributes and apply them on
- // preload.
- $.each(attributes, function () {
- if (this.name !== 'id' && this.name !== 'src') {
- largeVideoState.preload.attr(this.name, this.value);
- }
- });
-
- largeVideoState.preload.appendTo($('#largeVideoContainer'));
- $('#largeVideo').attr('id', 'previousLargeVideo');
- largeVideoState.preload.attr('id', 'largeVideo');
- $('#previousLargeVideo').remove();
-
- largeVideoState.preload.on('loadedmetadata', function (e) {
- currentVideoWidth = this.videoWidth;
- currentVideoHeight = this.videoHeight;
- VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight);
- });
- largeVideoState.preload = null;
- largeVideoState.preload_ssrc = 0;
- } else {
- APP.RTC.setVideoSrc($('#largeVideo')[0], largeVideoState.newSrc);
- }
-
- var videoTransform = document.getElementById('largeVideo')
- .style.webkitTransform;
-
- if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') {
- document.getElementById('largeVideo').style.webkitTransform
- = "scaleX(-1)";
- }
- else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') {
- document.getElementById('largeVideo').style.webkitTransform
- = "none";
- }
-
- // Change the way we'll be measuring and positioning large video
-
- VideoLayout.getVideoSize = largeVideoState.isDesktop
- ? getDesktopVideoSize
- : getCameraVideoSize;
- VideoLayout.getVideoPosition = largeVideoState.isDesktop
- ? getDesktopVideoPosition
- : getCameraVideoPosition;
-
-
- // Only if the large video is currently visible.
- // Disable previous dominant speaker video.
- if (largeVideoState.oldResourceJid) {
- VideoLayout.enableDominantSpeaker(
- largeVideoState.oldResourceJid,
- false);
- }
-
- // Enable new dominant speaker in the remote videos section.
- if (largeVideoState.userResourceJid) {
- VideoLayout.enableDominantSpeaker(
- largeVideoState.userResourceJid,
- true);
- }
-
- if (userChanged && largeVideoState.isVisible) {
- // using "this" should be ok because we're called
- // from within the fadeOut event.
- $(this).fadeIn(300);
- }
-
- if(userChanged) {
- Avatar.showUserAvatar(
- APP.xmpp.findJidFromResource(
- largeVideoState.oldResourceJid));
- }
-
- largeVideoState.updateInProgress = false;
- };
-
- if (userChanged) {
- $('#largeVideo').fadeOut(300, doUpdate);
- } else {
- doUpdate();
- }
- }
- } else {
- Avatar.showUserAvatar(
- APP.xmpp.findJidFromResource(
- largeVideoState.userResourceJid));
- }
-
- };
-
- my.handleVideoThumbClicked = function(videoSrc,
- noPinnedEndpointChangedEvent,
- resourceJid) {
- // Restore style for previously focused video
- var oldContainer = null;
- if(focusedVideoInfo) {
- var focusResourceJid = focusedVideoInfo.resourceJid;
- oldContainer = getParticipantContainer(focusResourceJid);
- }
-
- if (oldContainer) {
- oldContainer.removeClass("videoContainerFocused");
- }
-
- // Unlock current focused.
- if (focusedVideoInfo && focusedVideoInfo.src === videoSrc)
- {
- focusedVideoInfo = null;
- var dominantSpeakerVideo = null;
- // Enable the currently set dominant speaker.
- if (currentDominantSpeaker) {
- dominantSpeakerVideo
- = $('#participant_' + currentDominantSpeaker + '>video')
- .get(0);
-
- if (dominantSpeakerVideo) {
- VideoLayout.updateLargeVideo(
- APP.RTC.getVideoSrc(dominantSpeakerVideo),
- 1,
- currentDominantSpeaker);
- }
- }
-
- if (!noPinnedEndpointChangedEvent) {
- eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
- }
- return;
- }
-
- // Lock new video
- focusedVideoInfo = {
- src: videoSrc,
- resourceJid: resourceJid
- };
-
- // Update focused/pinned interface.
- if (resourceJid)
- {
- var container = getParticipantContainer(resourceJid);
- container.addClass("videoContainerFocused");
-
- if (!noPinnedEndpointChangedEvent) {
- eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
- }
- }
-
- if ($('#largeVideo').attr('src') === videoSrc &&
- VideoLayout.isLargeVideoOnTop()) {
- return;
- }
-
- // Triggers a "video.selected" event. The "false" parameter indicates
- // this isn't a prezi.
- $(document).trigger("video.selected", [false]);
-
- VideoLayout.updateLargeVideo(videoSrc, 1, resourceJid);
-
- $('audio').each(function (idx, el) {
- if (el.id.indexOf('mixedmslabel') !== -1) {
- el.volume = 0;
- el.volume = 1;
- }
- });
- };
-
- /**
- * Positions the large video.
- *
- * @param videoWidth the stream video width
- * @param videoHeight the stream video height
- */
- my.positionLarge = function (videoWidth, videoHeight) {
- var videoSpaceWidth = $('#videospace').width();
- var videoSpaceHeight = window.innerHeight;
-
- var videoSize = VideoLayout.getVideoSize(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight);
-
- var largeVideoWidth = videoSize[0];
- var largeVideoHeight = videoSize[1];
-
- var videoPosition = VideoLayout.getVideoPosition(largeVideoWidth,
- largeVideoHeight,
- videoSpaceWidth,
- videoSpaceHeight);
-
- var horizontalIndent = videoPosition[0];
- var verticalIndent = videoPosition[1];
-
- positionVideo($('#largeVideo'),
- largeVideoWidth,
- largeVideoHeight,
- horizontalIndent, verticalIndent);
- };
-
- /**
- * Shows/hides the large video.
- */
- my.setLargeVideoVisible = function(isVisible) {
- var resourceJid = largeVideoState.userResourceJid;
-
- if (isVisible) {
- $('#largeVideo').css({visibility: 'visible'});
- $('.watermark').css({visibility: 'visible'});
- VideoLayout.enableDominantSpeaker(resourceJid, true);
- }
- else {
- $('#largeVideo').css({visibility: 'hidden'});
- $('#activeSpeaker').css('visibility', 'hidden');
- $('.watermark').css({visibility: 'hidden'});
- VideoLayout.enableDominantSpeaker(resourceJid, false);
- if(focusedVideoInfo) {
- var focusResourceJid = focusedVideoInfo.resourceJid;
- var oldContainer = getParticipantContainer(focusResourceJid);
-
- if (oldContainer && oldContainer.length > 0) {
- oldContainer.removeClass("videoContainerFocused");
- }
- focusedVideoInfo = null;
- if(focusResourceJid) {
- Avatar.showUserAvatar(
- APP.xmpp.findJidFromResource(focusResourceJid));
- }
- }
- }
- };
-
- /**
- * Indicates if the large video is currently visible.
- *
- * @return true if visible, false - otherwise
- */
- my.isLargeVideoVisible = function() {
- return $('#largeVideo').is(':visible');
- };
-
- my.isLargeVideoOnTop = function () {
- var Etherpad = require("../etherpad/Etherpad");
- var Prezi = require("../prezi/Prezi");
- return !Prezi.isPresentationVisible() && !Etherpad.isVisible();
- };
-
- /**
- * Checks if container for participant identified by given peerJid exists
- * in the document and creates it eventually.
- *
- * @param peerJid peer Jid to check.
- * @param userId user email or id for setting the avatar
- *
- * @return Returns true if the peer container exists,
- * false - otherwise
- */
- my.ensurePeerContainerExists = function(peerJid, userId) {
- ContactList.ensureAddContact(peerJid, userId);
-
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var videoSpanId = 'participant_' + resourceJid;
-
- if (!$('#' + videoSpanId).length) {
- var container =
- VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId, userId);
- Avatar.setUserAvatar(peerJid, userId);
- // Set default display name.
- setDisplayName(videoSpanId);
-
- VideoLayout.connectionIndicators[videoSpanId] =
- new ConnectionIndicator(container, peerJid, VideoLayout);
-
- var nickfield = document.createElement('span');
- nickfield.className = "nick";
- nickfield.appendChild(document.createTextNode(resourceJid));
- container.appendChild(nickfield);
-
- // In case this is not currently in the last n we don't show it.
- if (localLastNCount
- && localLastNCount > 0
- && $('#remoteVideos>span').length >= localLastNCount + 2) {
- showPeerContainer(resourceJid, 'hide');
- }
- else
- VideoLayout.resizeThumbnails();
- }
- };
-
- my.addRemoteVideoContainer = function(peerJid, spanId) {
- var container = document.createElement('span');
- container.id = spanId;
- container.className = 'videocontainer';
- var remotes = document.getElementById('remoteVideos');
- remotes.appendChild(container);
- // If the peerJid is null then this video span couldn't be directly
- // associated with a participant (this could happen in the case of prezi).
- if (APP.xmpp.isModerator() && peerJid !== null)
- addRemoteVideoMenu(peerJid, container);
- AudioLevels.updateAudioLevelCanvas(peerJid, VideoLayout);
-
- return container;
- };
-
- /**
- * Creates an audio or video stream element.
- */
- my.createStreamElement = function (sid, stream) {
- var isVideo = stream.getVideoTracks().length > 0;
-
- var element = isVideo
- ? document.createElement('video')
- : document.createElement('audio');
- var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_')
- + sid + '_' + APP.RTC.getStreamID(stream);
-
- element.id = id;
- element.autoplay = true;
- element.oncontextmenu = function () { return false; };
-
- return element;
- };
-
- my.addRemoteStreamElement
- = function (container, sid, stream, peerJid, thessrc) {
- var newElementId = null;
-
- var isVideo = stream.getVideoTracks().length > 0;
-
- if (container) {
- var streamElement = VideoLayout.createStreamElement(sid, stream);
- newElementId = streamElement.id;
-
- container.appendChild(streamElement);
-
- var sel = $('#' + newElementId);
- sel.hide();
-
- // If the container is currently visible we attach the stream.
- if (!isVideo
- || (container.offsetParent !== null && isVideo)) {
- var videoStream = APP.simulcast.getReceivingVideoStream(stream);
- APP.RTC.attachMediaStream(sel, videoStream);
-
- if (isVideo)
- waitForRemoteVideo(sel, thessrc, stream, peerJid);
- }
-
- stream.onended = function () {
- console.log('stream ended', this);
-
- VideoLayout.removeRemoteStreamElement(
- stream, isVideo, container);
-
- // NOTE(gp) it seems that under certain circumstances, the
- // onended event is not fired and thus the contact list is not
- // updated.
- //
- // The onended event of a stream should be fired when the SSRCs
- // corresponding to that stream are removed from the SDP; but
- // this doesn't seem to always be the case, resulting in ghost
- // contacts.
- //
- // In an attempt to fix the ghost contacts problem, I'm moving
- // the removeContact() method call in app.js, inside the
- // 'muc.left' event handler.
-
- //if (peerJid)
- // ContactList.removeContact(peerJid);
- };
-
- // Add click handler.
- container.onclick = function (event) {
- /*
- * FIXME It turns out that videoThumb may not exist (if there is
- * no actual video).
- */
- var videoThumb = $('#' + container.id + '>video').get(0);
- if (videoThumb) {
- VideoLayout.handleVideoThumbClicked(
- APP.RTC.getVideoSrc(videoThumb),
- false,
- Strophe.getResourceFromJid(peerJid));
- }
-
- event.stopPropagation();
- event.preventDefault();
- return false;
- };
-
- // Add hover handler
- $(container).hover(
- function() {
- VideoLayout.showDisplayName(container.id, true);
- },
- function() {
- var videoSrc = null;
- if ($('#' + container.id + '>video')
- && $('#' + container.id + '>video').length > 0) {
- videoSrc = APP.RTC.getVideoSrc($('#' + container.id + '>video').get(0));
- }
-
- // If the video has been "pinned" by the user we want to
- // keep the display name on place.
- if (!VideoLayout.isLargeVideoVisible()
- || videoSrc !== APP.RTC.getVideoSrc($('#largeVideo')[0]))
- VideoLayout.showDisplayName(container.id, false);
- }
- );
- }
-
- return newElementId;
- };
-
- /**
- * Removes the remote stream element corresponding to the given stream and
- * parent container.
- *
- * @param stream the stream
- * @param isVideo true if given stream is a video one.
- * @param container
- */
- my.removeRemoteStreamElement = function (stream, isVideo, container) {
- if (!container)
- return;
-
- var select = null;
- var removedVideoSrc = null;
- if (isVideo) {
- select = $('#' + container.id + '>video');
- removedVideoSrc = APP.RTC.getVideoSrc(select.get(0));
- }
- else
- select = $('#' + container.id + '>audio');
-
-
- // Mark video as removed to cancel waiting loop(if video is removed
- // before has started)
- select.removed = true;
- select.remove();
-
- var audioCount = $('#' + container.id + '>audio').length;
- var videoCount = $('#' + container.id + '>video').length;
-
- if (!audioCount && !videoCount) {
- console.log("Remove whole user", container.id);
- if(VideoLayout.connectionIndicators[container.id])
- VideoLayout.connectionIndicators[container.id].remove();
- // Remove whole container
- container.remove();
-
- UIUtil.playSoundNotification('userLeft');
- VideoLayout.resizeThumbnails();
- }
-
- if (removedVideoSrc)
- VideoLayout.updateRemovedVideo(removedVideoSrc);
- };
-
- /**
- * Show/hide peer container for the given resourceJid.
- */
- function showPeerContainer(resourceJid, state) {
- var peerContainer = $('#participant_' + resourceJid);
-
- if (!peerContainer)
- return;
-
- var isHide = state === 'hide';
- var resizeThumbnails = false;
-
- if (!isHide) {
- if (!peerContainer.is(':visible')) {
- resizeThumbnails = true;
- peerContainer.show();
- }
-
- var jid = APP.xmpp.findJidFromResource(resourceJid);
- if (state == 'show')
- {
- // peerContainer.css('-webkit-filter', '');
-
- Avatar.showUserAvatar(jid, false);
- }
- else // if (state == 'avatar')
- {
- // peerContainer.css('-webkit-filter', 'grayscale(100%)');
- Avatar.showUserAvatar(jid, true);
- }
- }
- else if (peerContainer.is(':visible') && isHide)
- {
- resizeThumbnails = true;
- peerContainer.hide();
- if(VideoLayout.connectionIndicators['participant_' + resourceJid])
- VideoLayout.connectionIndicators['participant_' + resourceJid].hide();
- }
-
- if (resizeThumbnails) {
- VideoLayout.resizeThumbnails();
- }
-
- // We want to be able to pin a participant from the contact list, even
- // if he's not in the lastN set!
- // ContactList.setClickable(resourceJid, !isHide);
-
- };
-
- my.inputDisplayNameHandler = function (name) {
- NicknameHandler.setNickname(name);
-
- if (!$('#localDisplayName').is(":visible")) {
- if (NicknameHandler.getNickname())
- {
- var meHTML = APP.translation.generateTranslatonHTML("me");
- $('#localDisplayName').html(NicknameHandler.getNickname() + " (" + meHTML + ")");
- }
- else
- {
- var defaultHTML = APP.translation.generateTranslatonHTML(
- interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
- $('#localDisplayName')
- .html(defaultHTML);
- }
- $('#localDisplayName').show();
- }
-
- $('#editDisplayName').hide();
- };
-
- /**
- * Shows/hides the display name on the remote video.
- * @param videoSpanId the identifier of the video span element
- * @param isShow indicates if the display name should be shown or hidden
- */
- my.showDisplayName = function(videoSpanId, isShow) {
- var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
- if (isShow) {
- if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
- nameSpan.setAttribute("style", "display:inline-block;");
- }
- else {
- if (nameSpan)
- nameSpan.setAttribute("style", "display:none;");
- }
- };
-
- /**
- * Shows the presence status message for the given video.
- */
- my.setPresenceStatus = function (videoSpanId, statusMsg) {
-
- if (!$('#' + videoSpanId).length) {
- // No container
- return;
- }
-
- var statusSpan = $('#' + videoSpanId + '>span.status');
- if (!statusSpan.length) {
- //Add status span
- statusSpan = document.createElement('span');
- statusSpan.className = 'status';
- statusSpan.id = videoSpanId + '_status';
- $('#' + videoSpanId)[0].appendChild(statusSpan);
-
- statusSpan = $('#' + videoSpanId + '>span.status');
- }
-
- // Display status
- if (statusMsg && statusMsg.length) {
- $('#' + videoSpanId + '_status').text(statusMsg);
- statusSpan.get(0).setAttribute("style", "display:inline-block;");
- }
- else {
- // Hide
- statusSpan.get(0).setAttribute("style", "display:none;");
- }
- };
-
- /**
- * Shows a visual indicator for the moderator of the conference.
- */
- my.showModeratorIndicator = function () {
-
- var isModerator = APP.xmpp.isModerator();
- if (isModerator) {
- var indicatorSpan = $('#localVideoContainer .focusindicator');
-
- if (indicatorSpan.children().length === 0)
- {
- createModeratorIndicatorElement(indicatorSpan[0]);
- //translates text in focus indicator
- APP.translation.translateElement($('#localVideoContainer .focusindicator'));
- }
- }
-
- var members = APP.xmpp.getMembers();
-
- Object.keys(members).forEach(function (jid) {
-
- if (Strophe.getResourceFromJid(jid) === 'focus') {
- // Skip server side focus
- return;
- }
-
- var resourceJid = Strophe.getResourceFromJid(jid);
- var videoSpanId = 'participant_' + resourceJid;
- var videoContainer = document.getElementById(videoSpanId);
-
- if (!videoContainer) {
- console.error("No video container for " + jid);
- return;
- }
-
- var member = members[jid];
-
- if (member.role === 'moderator') {
- // Remove menu if peer is moderator
- var menuSpan = $('#' + videoSpanId + '>span.remotevideomenu');
- if (menuSpan.length) {
- removeRemoteVideoMenu(videoSpanId);
- }
- // Show moderator indicator
- var indicatorSpan
- = $('#' + videoSpanId + ' .focusindicator');
-
- if (!indicatorSpan || indicatorSpan.length === 0) {
- indicatorSpan = document.createElement('span');
- indicatorSpan.className = 'focusindicator';
-
- videoContainer.appendChild(indicatorSpan);
-
- createModeratorIndicatorElement(indicatorSpan);
- //translates text in focus indicators
- APP.translation.translateElement($('#' + videoSpanId + ' .focusindicator'));
- }
- } else if (isModerator) {
- // We are moderator, but user is not - add menu
- if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
- addRemoteVideoMenu(
- jid,
- document.getElementById('participant_' + resourceJid));
- }
- }
- });
- };
-
- /**
- * Shows video muted indicator over small videos.
- */
- my.showVideoIndicator = function(videoSpanId, isMuted) {
- var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
-
- if (isMuted === 'false') {
- if (videoMutedSpan.length > 0) {
- videoMutedSpan.remove();
- }
- }
- else {
- if(videoMutedSpan.length == 0) {
- videoMutedSpan = document.createElement('span');
- videoMutedSpan.className = 'videoMuted';
-
- $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
-
- var mutedIndicator = document.createElement('i');
- mutedIndicator.className = 'icon-camera-disabled';
- UIUtil.setTooltip(mutedIndicator,
- "videothumbnail.videomute",
- "top");
- videoMutedSpan.appendChild(mutedIndicator);
- //translate texts for muted indicator
- APP.translation.translateElement($('#' + videoSpanId + " > span > i"));
- }
-
- VideoLayout.updateMutePosition(videoSpanId);
-
- }
- };
-
- my.updateMutePosition = function (videoSpanId) {
- var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
- var connectionIndicator = $('#' + videoSpanId + '>div.connectionindicator');
- var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
- if(connectionIndicator.length > 0
- && connectionIndicator[0].style.display != "none") {
- audioMutedSpan.css({right: "23px"});
- videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
- }
- else
- {
- audioMutedSpan.css({right: "0px"});
- videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
- }
- }
- /**
- * Shows audio muted indicator over small videos.
- * @param {string} isMuted
- */
- my.showAudioIndicator = function(videoSpanId, isMuted) {
- var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
-
- if (isMuted === 'false') {
- if (audioMutedSpan.length > 0) {
- audioMutedSpan.popover('hide');
- audioMutedSpan.remove();
- }
- }
- else {
- if(audioMutedSpan.length == 0 ) {
- audioMutedSpan = document.createElement('span');
- audioMutedSpan.className = 'audioMuted';
- UIUtil.setTooltip(audioMutedSpan,
- "videothumbnail.mute",
- "top");
-
- $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
- APP.translation.translateElement($('#' + videoSpanId + " > span"));
- var mutedIndicator = document.createElement('i');
- mutedIndicator.className = 'icon-mic-disabled';
- audioMutedSpan.appendChild(mutedIndicator);
-
- }
- VideoLayout.updateMutePosition(videoSpanId);
- }
- };
-
- /*
- * Shows or hides the audio muted indicator over the local thumbnail video.
- * @param {boolean} isMuted
- */
- my.showLocalAudioIndicator = function(isMuted) {
- VideoLayout.showAudioIndicator('localVideoContainer', isMuted.toString());
- };
-
- /**
- * Resizes the large video container.
- */
- my.resizeLargeVideoContainer = function () {
- Chat.resizeChat();
- var availableHeight = window.innerHeight;
- var availableWidth = UIUtil.getAvailableVideoWidth();
-
- if (availableWidth < 0 || availableHeight < 0) return;
-
- $('#videospace').width(availableWidth);
- $('#videospace').height(availableHeight);
- $('#largeVideoContainer').width(availableWidth);
- $('#largeVideoContainer').height(availableHeight);
-
- var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
- var top = availableHeight / 2 - avatarSize / 4 * 3;
- $('#activeSpeaker').css('top', top);
-
- VideoLayout.resizeThumbnails();
- };
-
- /**
- * Resizes thumbnails.
- */
- my.resizeThumbnails = function() {
- var videoSpaceWidth = $('#remoteVideos').width();
-
- var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
- var width = thumbnailSize[0];
- var height = thumbnailSize[1];
-
- // size videos so that while keeping AR and max height, we have a
- // nice fit
- $('#remoteVideos').height(height);
- $('#remoteVideos>span').width(width);
- $('#remoteVideos>span').height(height);
-
- $('.userAvatar').css('left', (width - height) / 2);
-
-
-
- $(document).trigger("remotevideo.resized", [width, height]);
- };
-
- /**
- * Enables the dominant speaker UI.
- *
- * @param resourceJid the jid indicating the video element to
- * activate/deactivate
- * @param isEnable indicates if the dominant speaker should be enabled or
- * disabled
- */
- my.enableDominantSpeaker = function(resourceJid, isEnable) {
-
- var videoSpanId = null;
- var videoContainerId = null;
- if (resourceJid
- === APP.xmpp.myResource()) {
- videoSpanId = 'localVideoWrapper';
- videoContainerId = 'localVideoContainer';
- }
- else {
- videoSpanId = 'participant_' + resourceJid;
- videoContainerId = videoSpanId;
- }
-
- var displayName = resourceJid;
- var nameSpan = $('#' + videoContainerId + '>span.displayname');
- if (nameSpan.length > 0)
- displayName = nameSpan.html();
-
- console.log("UI enable dominant speaker",
- displayName,
- resourceJid,
- isEnable);
-
- videoSpan = document.getElementById(videoContainerId);
-
- if (!videoSpan) {
- return;
- }
-
- var video = $('#' + videoSpanId + '>video');
-
- if (video && video.length > 0) {
- if (isEnable) {
- var isLargeVideoVisible = VideoLayout.isLargeVideoOnTop();
- VideoLayout.showDisplayName(videoContainerId, isLargeVideoVisible);
-
- if (!videoSpan.classList.contains("dominantspeaker"))
- videoSpan.classList.add("dominantspeaker");
- }
- else {
- VideoLayout.showDisplayName(videoContainerId, false);
-
- if (videoSpan.classList.contains("dominantspeaker"))
- videoSpan.classList.remove("dominantspeaker");
- }
-
- Avatar.showUserAvatar(
- APP.xmpp.findJidFromResource(resourceJid));
- }
- };
-
- /**
- * Calculates the thumbnail size.
- *
- * @param videoSpaceWidth the width of the video space
- */
- my.calculateThumbnailSize = function (videoSpaceWidth) {
- // Calculate the available height, which is the inner window height minus
- // 39px for the header minus 2px for the delimiter lines on the top and
- // bottom of the large video, minus the 36px space inside the remoteVideos
- // container used for highlighting shadow.
- var availableHeight = 100;
-
- var numvids = $('#remoteVideos>span:visible').length;
- if (localLastNCount && localLastNCount > 0) {
- numvids = Math.min(localLastNCount + 1, numvids);
- }
-
- // Remove the 3px borders arround videos and border around the remote
- // videos area and the 4 pixels between the local video and the others
- //TODO: Find out where the 4 pixels come from and remove them
- var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4;
-
- var availableWidth = availableWinWidth / numvids;
- var aspectRatio = 16.0 / 9.0;
- var maxHeight = Math.min(160, availableHeight);
- availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
- if (availableHeight < availableWidth / aspectRatio) {
- availableWidth = Math.floor(availableHeight * aspectRatio);
- }
-
- return [availableWidth, availableHeight];
- };
-
- /**
- * Updates the remote video menu.
- *
- * @param jid the jid indicating the video for which we're adding a menu.
- * @param isMuted indicates the current mute state
- */
- my.updateRemoteVideoMenu = function(jid, isMuted) {
- var muteMenuItem
- = $('#remote_popupmenu_'
- + Strophe.getResourceFromJid(jid)
- + '>li>a.mutelink');
-
- var mutedIndicator = " ";
-
- if (muteMenuItem.length) {
- var muteLink = muteMenuItem.get(0);
-
- if (isMuted === 'true') {
- muteLink.innerHTML = mutedIndicator + ' Muted';
- muteLink.className = 'mutelink disabled';
- }
- else {
- muteLink.innerHTML = mutedIndicator + ' Mute';
- muteLink.className = 'mutelink';
- }
- }
- };
-
- /**
- * Returns the current dominant speaker resource jid.
- */
- my.getDominantSpeakerResourceJid = function () {
- return currentDominantSpeaker;
- };
-
- /**
- * Returns the corresponding resource jid to the given peer container
- * DOM element.
- *
- * @return the corresponding resource jid to the given peer container
- * DOM element
- */
- my.getPeerContainerResourceJid = function (containerElement) {
- var i = containerElement.id.indexOf('participant_');
-
- if (i >= 0)
- return containerElement.id.substring(i + 12);
- };
-
- /**
- * On contact list item clicked.
- */
- $(ContactList).bind('contactclicked', function(event, jid) {
- if (!jid) {
- return;
- }
-
- var resource = Strophe.getResourceFromJid(jid);
- var videoContainer = $("#participant_" + resource);
- if (videoContainer.length > 0) {
- var videoThumb = $('video', videoContainer).get(0);
- // It is not always the case that a videoThumb exists (if there is
- // no actual video).
- if (videoThumb) {
- if (videoThumb.src && videoThumb.src != '') {
-
- // We have a video src, great! Let's update the large video
- // now.
-
- VideoLayout.handleVideoThumbClicked(
- videoThumb.src,
- false,
- Strophe.getResourceFromJid(jid));
- } else {
-
- // If we don't have a video src for jid, there's absolutely
- // no point in calling handleVideoThumbClicked; Quite
- // simply, it won't work because it needs an src to attach
- // to the large video.
- //
- // Instead, we trigger the pinned endpoint changed event to
- // let the bridge adjust its lastN set for myjid and store
- // the pinned user in the lastNPickupJid variable to be
- // picked up later by the lastN changed event handler.
-
- lastNPickupJid = jid;
- eventEmitter.emit(UIEvents.PINNED_ENDPOINT,
- Strophe.getResourceFromJid(jid));
- }
- } else if (jid == APP.xmpp.myJid()) {
- $("#localVideoContainer").click();
- }
- }
- });
-
- /**
- * On audio muted event.
- */
- $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
- /*
- // FIXME: but focus can not mute in this case ? - check
- if (jid === xmpp.myJid()) {
-
- // The local mute indicator is controlled locally
- return;
- }*/
- var videoSpanId = null;
- if (jid === APP.xmpp.myJid()) {
- videoSpanId = 'localVideoContainer';
- } else {
- VideoLayout.ensurePeerContainerExists(jid);
- videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
- }
-
- mutedAudios[jid] = isMuted;
-
- if (APP.xmpp.isModerator()) {
- VideoLayout.updateRemoteVideoMenu(jid, isMuted);
- }
-
- if (videoSpanId)
- VideoLayout.showAudioIndicator(videoSpanId, isMuted);
- });
-
- /**
- * On video muted event.
- */
- $(document).bind('videomuted.muc', function (event, jid, value) {
- var isMuted = (value === "true");
- if(jid !== APP.xmpp.myJid() && !APP.RTC.muteRemoteVideoStream(jid, isMuted))
- return;
-
- Avatar.showUserAvatar(jid, isMuted);
- var videoSpanId = null;
- if (jid === APP.xmpp.myJid()) {
- videoSpanId = 'localVideoContainer';
- } else {
- VideoLayout.ensurePeerContainerExists(jid);
- videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
- }
-
- if (videoSpanId)
- VideoLayout.showVideoIndicator(videoSpanId, value);
- });
-
- /**
- * Display name changed.
- */
- my.onDisplayNameChanged =
- function (jid, displayName, status) {
- if (jid === 'localVideoContainer'
- || jid === APP.xmpp.myJid()) {
- setDisplayName('localVideoContainer',
- displayName);
- } else {
- VideoLayout.ensurePeerContainerExists(jid);
- setDisplayName(
- 'participant_' + Strophe.getResourceFromJid(jid),
- displayName,
- status);
- }
-
- };
-
- /**
- * On dominant speaker changed event.
- */
- my.onDominantSpeakerChanged = function (resourceJid) {
- // We ignore local user events.
- if (resourceJid
- === APP.xmpp.myResource())
- return;
-
- var members = APP.xmpp.getMembers();
- // Update the current dominant speaker.
- if (resourceJid !== currentDominantSpeaker) {
- var oldSpeakerVideoSpanId = "participant_" + currentDominantSpeaker,
- newSpeakerVideoSpanId = "participant_" + resourceJid;
- var currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker);
- var newJID = APP.xmpp.findJidFromResource(resourceJid);
- if(currentDominantSpeaker && (!members || !members[currentJID] ||
- !members[currentJID].displayName)) {
- setDisplayName(oldSpeakerVideoSpanId, null);
- }
- if(resourceJid && (!members || !members[newJID] ||
- !members[newJID].displayName)) {
- setDisplayName(newSpeakerVideoSpanId, null,
- interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME);
- }
- currentDominantSpeaker = resourceJid;
- } else {
- return;
- }
-
- // Obtain container for new dominant speaker.
- var container = document.getElementById(
- 'participant_' + resourceJid);
-
- // Local video will not have container found, but that's ok
- // since we don't want to switch to local video.
- if (container && !focusedVideoInfo)
- {
- var video = container.getElementsByTagName("video");
-
- // Update the large video if the video source is already available,
- // otherwise wait for the "videoactive.jingle" event.
- if (video.length && video[0].currentTime > 0)
- VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(video[0]), resourceJid);
- }
- };
-
- /**
- * On last N change event.
- *
- * @param lastNEndpoints the list of last N endpoints
- * @param endpointsEnteringLastN the list currently entering last N
- * endpoints
- */
- my.onLastNEndpointsChanged = function ( lastNEndpoints,
- endpointsEnteringLastN,
- stream) {
- if (lastNCount !== lastNEndpoints.length)
- lastNCount = lastNEndpoints.length;
-
- lastNEndpointsCache = lastNEndpoints;
-
- // Say A, B, C, D, E, and F are in a conference and LastN = 3.
- //
- // If LastN drops to, say, 2, because of adaptivity, then E should see
- // thumbnails for A, B and C. A and B are in E's server side LastN set,
- // so E sees them. C is only in E's local LastN set.
- //
- // If F starts talking and LastN = 3, then E should see thumbnails for
- // F, A, B. B gets "ejected" from E's server side LastN set, but it
- // enters E's local LastN ejecting C.
-
- // Increase the local LastN set size, if necessary.
- if (lastNCount > localLastNCount) {
- localLastNCount = lastNCount;
- }
-
- // Update the local LastN set preserving the order in which the
- // endpoints appeared in the LastN/local LastN set.
-
- var nextLocalLastNSet = lastNEndpoints.slice(0);
- for (var i = 0; i < localLastNSet.length; i++) {
- if (nextLocalLastNSet.length >= localLastNCount) {
- break;
- }
-
- var resourceJid = localLastNSet[i];
- if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
- nextLocalLastNSet.push(resourceJid);
- }
- }
-
- localLastNSet = nextLocalLastNSet;
-
- var updateLargeVideo = false;
-
- // Handle LastN/local LastN changes.
- $('#remoteVideos>span').each(function( index, element ) {
- var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
-
- var isReceived = true;
- if (resourceJid
- && lastNEndpoints.indexOf(resourceJid) < 0
- && localLastNSet.indexOf(resourceJid) < 0) {
- console.log("Remove from last N", resourceJid);
- showPeerContainer(resourceJid, 'hide');
- isReceived = false;
- } else if (resourceJid
- && $('#participant_' + resourceJid).is(':visible')
- && lastNEndpoints.indexOf(resourceJid) < 0
- && localLastNSet.indexOf(resourceJid) >= 0) {
- showPeerContainer(resourceJid, 'avatar');
- isReceived = false;
- }
-
- if (!isReceived) {
- // resourceJid has dropped out of the server side lastN set, so
- // it is no longer being received. If resourceJid was being
- // displayed in the large video we have to switch to another
- // user.
- var largeVideoResource = largeVideoState.userResourceJid;
- if (!updateLargeVideo && resourceJid === largeVideoResource) {
- updateLargeVideo = true;
- }
- }
- });
-
- if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
- endpointsEnteringLastN = lastNEndpoints;
-
- if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
- endpointsEnteringLastN.forEach(function (resourceJid) {
-
- var isVisible = $('#participant_' + resourceJid).is(':visible');
- showPeerContainer(resourceJid, 'show');
- if (!isVisible) {
- console.log("Add to last N", resourceJid);
-
- var jid = APP.xmpp.findJidFromResource(resourceJid);
- var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
- var sel = $('#participant_' + resourceJid + '>video');
-
- var videoStream = APP.simulcast.getReceivingVideoStream(
- mediaStream.stream);
- APP.RTC.attachMediaStream(sel, videoStream);
- if (lastNPickupJid == mediaStream.peerjid) {
- // Clean up the lastN pickup jid.
- lastNPickupJid = null;
-
- // Don't fire the events again, they've already
- // been fired in the contact list click handler.
- VideoLayout.handleVideoThumbClicked(
- $(sel).attr('src'),
- false,
- Strophe.getResourceFromJid(mediaStream.peerjid));
-
- updateLargeVideo = false;
- }
- waitForRemoteVideo(sel, mediaStream.ssrc, mediaStream.stream, resourceJid);
- }
- })
- }
-
- // The endpoint that was being shown in the large video has dropped out
- // of the lastN set and there was no lastN pickup jid. We need to update
- // the large video now.
-
- if (updateLargeVideo) {
-
- var resource, container, src;
- var myResource
- = APP.xmpp.myResource();
-
- // Find out which endpoint to show in the large video.
- for (var i = 0; i < lastNEndpoints.length; i++) {
- resource = lastNEndpoints[i];
- if (!resource || resource === myResource)
- continue;
-
- container = $("#participant_" + resource);
- if (container.length == 0)
- continue;
-
- src = $('video', container).attr('src');
- if (!src)
- continue;
-
- // videoSrcToSsrc needs to be update for this call to succeed.
- VideoLayout.updateLargeVideo(src);
- break;
-
- }
- }
- };
-
- my.onSimulcastLayersChanging = function (endpointSimulcastLayers) {
- endpointSimulcastLayers.forEach(function (esl) {
-
- var resource = esl.endpoint;
-
- // if lastN is enabled *and* the endpoint is *not* in the lastN set,
- // then ignore the event (= do not preload anything).
- //
- // The bridge could probably stop sending this message if it's for
- // an endpoint that's not in lastN.
-
- if (lastNCount != -1
- && (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
- return;
- }
-
- var primarySSRC = esl.simulcastLayer.primarySSRC;
-
- // Get session and stream from primary ssrc.
- var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
- var sid = res.sid;
- var electedStream = res.stream;
-
- if (sid && electedStream) {
- var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
-
- console.info([esl, primarySSRC, msid, sid, electedStream]);
-
- var preload = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC)) == largeVideoState.userResourceJid);
-
- if (preload) {
- if (largeVideoState.preload)
- {
- $(largeVideoState.preload).remove();
- }
- console.info('Preloading remote video');
- largeVideoState.preload = $(' ');
- // ssrcs are unique in an rtp session
- largeVideoState.preload_ssrc = primarySSRC;
-
- APP.RTC.attachMediaStream(largeVideoState.preload, electedStream)
- }
-
- } else {
- console.error('Could not find a stream or a session.', sid, electedStream);
- }
- });
- };
-
- /**
- * On simulcast layers changed event.
- */
- my.onSimulcastLayersChanged = function (endpointSimulcastLayers) {
- endpointSimulcastLayers.forEach(function (esl) {
-
- var resource = esl.endpoint;
-
- // if lastN is enabled *and* the endpoint is *not* in the lastN set,
- // then ignore the event (= do not change large video/thumbnail
- // SRCs).
- //
- // Note that even if we ignore the "changed" event in this event
- // handler, the bridge must continue sending these events because
- // the simulcast code in simulcast.js uses it to know what's going
- // to be streamed by the bridge when/if the endpoint gets back into
- // the lastN set.
-
- if (lastNCount != -1
- && (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
- return;
- }
-
- var primarySSRC = esl.simulcastLayer.primarySSRC;
-
- // Get session and stream from primary ssrc.
- var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
- var sid = res.sid;
- var electedStream = res.stream;
-
- if (sid && electedStream) {
- var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
-
- console.info('Switching simulcast substream.');
- console.info([esl, primarySSRC, msid, sid, electedStream]);
-
- var msidParts = msid.split(' ');
- var selRemoteVideo = $(['#', 'remoteVideo_', sid, '_', msidParts[0]].join(''));
-
- var updateLargeVideo = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC))
- == largeVideoState.userResourceJid);
- var updateFocusedVideoSrc = (focusedVideoInfo && focusedVideoInfo.src && focusedVideoInfo.src != '' &&
- (APP.RTC.getVideoSrc(selRemoteVideo[0]) == focusedVideoInfo.src));
-
- var electedStreamUrl;
- if (largeVideoState.preload_ssrc == primarySSRC)
- {
- APP.RTC.setVideoSrc(selRemoteVideo[0], APP.RTC.getVideoSrc(largeVideoState.preload[0]));
- }
- else
- {
- if (largeVideoState.preload
- && largeVideoState.preload != null) {
- $(largeVideoState.preload).remove();
- }
-
- largeVideoState.preload_ssrc = 0;
-
- APP.RTC.attachMediaStream(selRemoteVideo, electedStream);
- }
-
- var jid = APP.xmpp.getJidFromSSRC(primarySSRC);
-
- if (updateLargeVideo) {
- VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(selRemoteVideo[0]), null,
- Strophe.getResourceFromJid(jid));
- }
-
- if (updateFocusedVideoSrc) {
- focusedVideoInfo.src = APP.RTC.getVideoSrc(selRemoteVideo[0]);
- }
-
- var videoId;
- if(resource == APP.xmpp.myResource())
- {
- videoId = "localVideoContainer";
- }
- else
- {
- videoId = "participant_" + resource;
- }
- var connectionIndicator = VideoLayout.connectionIndicators[videoId];
- if(connectionIndicator)
- connectionIndicator.updatePopoverData();
-
- } else {
- console.error('Could not find a stream or a sid.', sid, electedStream);
- }
- });
- };
-
- /**
- * Updates local stats
- * @param percent
- * @param object
- */
- my.updateLocalConnectionStats = function (percent, object) {
- var resolution = null;
- if(object.resolution !== null)
- {
- resolution = object.resolution;
- object.resolution = resolution[APP.xmpp.myJid()];
- delete resolution[APP.xmpp.myJid()];
- }
- updateStatsIndicator("localVideoContainer", percent, object);
- for(var jid in resolution)
- {
- if(resolution[jid] === null)
- continue;
- var id = 'participant_' + Strophe.getResourceFromJid(jid);
- if(VideoLayout.connectionIndicators[id])
- {
- VideoLayout.connectionIndicators[id].updateResolution(resolution[jid]);
- }
- }
-
- };
-
- /**
- * Updates remote stats.
- * @param jid the jid associated with the stats
- * @param percent the connection quality percent
- * @param object the stats data
- */
- my.updateConnectionStats = function (jid, percent, object) {
- var resourceJid = Strophe.getResourceFromJid(jid);
-
- var videoSpanId = 'participant_' + resourceJid;
- updateStatsIndicator(videoSpanId, percent, object);
- };
-
- /**
- * Removes the connection
- * @param jid
- */
- my.removeConnectionIndicator = function (jid) {
- if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)])
- VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].remove();
- };
-
- /**
- * Hides the connection indicator
- * @param jid
- */
- my.hideConnectionIndicator = function (jid) {
- if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)])
- VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].hide();
- };
-
- /**
- * Hides all the indicators
- */
- my.onStatsStop = function () {
- for(var indicator in VideoLayout.connectionIndicators)
- {
- VideoLayout.connectionIndicators[indicator].hideIndicator();
- }
- };
-
- my.participantLeft = function (jid) {
- // Unlock large video
- if (focusedVideoInfo && focusedVideoInfo.jid === jid)
- {
- console.info("Focused video owner has left the conference");
- focusedVideoInfo = null;
- }
- }
-
- my.onVideoTypeChanged = function (jid) {
- if(jid &&
- Strophe.getResourceFromJid(jid) === largeVideoState.userResourceJid)
- {
- largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop(jid);
- VideoLayout.getVideoSize = largeVideoState.isDesktop
- ? getDesktopVideoSize
- : getCameraVideoSize;
- VideoLayout.getVideoPosition = largeVideoState.isDesktop
- ? getDesktopVideoPosition
- : getCameraVideoPosition;
- VideoLayout.positionLarge(null, null);
- }
- }
-
- return my;
-}(VideoLayout || {}));
-
+},{"../util/JitsiPopover":28}],33:[function(require,module,exports){
+var AudioLevels = require("../audio_levels/AudioLevels");
+var Avatar = require("../avatar/Avatar");
+var Chat = require("../side_pannels/chat/Chat");
+var ContactList = require("../side_pannels/contactlist/ContactList");
+var UIUtil = require("../util/UIUtil");
+var ConnectionIndicator = require("./ConnectionIndicator");
+var NicknameHandler = require("../util/NicknameHandler");
+var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
+var UIEvents = require("../../../service/UI/UIEvents");
+
+var currentDominantSpeaker = null;
+var lastNCount = config.channelLastN;
+var localLastNCount = config.channelLastN;
+var localLastNSet = [];
+var lastNEndpointsCache = [];
+var lastNPickupJid = null;
+var largeVideoState = {
+ updateInProgress: false,
+ newSrc: ''
+};
+
+var eventEmitter = null;
+
+/**
+ * Currently focused video "src"(displayed in large video).
+ * @type {String}
+ */
+var focusedVideoInfo = null;
+
+/**
+ * Indicates if we have muted our audio before the conference has started.
+ * @type {boolean}
+ */
+var preMuted = false;
+
+var mutedAudios = {};
+
+var flipXLocalVideo = true;
+var currentVideoWidth = null;
+var currentVideoHeight = null;
+
+var localVideoSrc = null;
+
+function videoactive( videoelem) {
+ if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
+ // ignore mixedmslabela0 and v0
+
+ videoelem.show();
+ VideoLayout.resizeThumbnails();
+
+ var videoParent = videoelem.parent();
+ var parentResourceJid = null;
+ if (videoParent)
+ parentResourceJid
+ = VideoLayout.getPeerContainerResourceJid(videoParent[0]);
+
+ // Update the large video to the last added video only if there's no
+ // current dominant, focused speaker or prezi playing or update it to
+ // the current dominant speaker.
+ if ((!focusedVideoInfo &&
+ !VideoLayout.getDominantSpeakerResourceJid() &&
+ !require("../prezi/Prezi").isPresentationVisible()) ||
+ (parentResourceJid &&
+ VideoLayout.getDominantSpeakerResourceJid() === parentResourceJid)) {
+ VideoLayout.updateLargeVideo(
+ APP.RTC.getVideoSrc(videoelem[0]),
+ 1,
+ parentResourceJid);
+ }
+
+ VideoLayout.showModeratorIndicator();
+ }
+}
+
+function waitForRemoteVideo(selector, ssrc, stream, jid) {
+ // XXX(gp) so, every call to this function is *always* preceded by a call
+ // to the RTC.attachMediaStream() function but that call is *not* followed
+ // by an update to the videoSrcToSsrc map!
+ //
+ // The above way of doing things results in video SRCs that don't correspond
+ // to any SSRC for a short period of time (to be more precise, for as long
+ // the waitForRemoteVideo takes to complete). This causes problems (see
+ // bellow).
+ //
+ // I'm wondering why we need to do that; i.e. why call RTC.attachMediaStream()
+ // a second time in here and only then update the videoSrcToSsrc map? Why
+ // not simply update the videoSrcToSsrc map when the RTC.attachMediaStream()
+ // is called the first time? I actually do that in the lastN changed event
+ // handler because the "orphan" video SRC is causing troubles there. The
+ // purpose of this method would then be to fire the "videoactive.jingle".
+ //
+ // Food for though I guess :-)
+
+ if (selector.removed || !selector.parent().is(":visible")) {
+ console.warn("Media removed before had started", selector);
+ return;
+ }
+
+ if (stream.id === 'mixedmslabel') return;
+
+ if (selector[0].currentTime > 0) {
+ var videoStream = APP.simulcast.getReceivingVideoStream(stream);
+ APP.RTC.attachMediaStream(selector, videoStream); // FIXME: why do i have to do this for FF?
+ videoactive(selector);
+ } else {
+ setTimeout(function () {
+ waitForRemoteVideo(selector, ssrc, stream, jid);
+ }, 250);
+ }
+}
+
+/**
+ * Returns an array of the video horizontal and vertical indents,
+ * so that if fits its parent.
+ *
+ * @return an array with 2 elements, the horizontal indent and the vertical
+ * indent
+ */
+function getCameraVideoPosition(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight) {
+ // Parent height isn't completely calculated when we position the video in
+ // full screen mode and this is why we use the screen height in this case.
+ // Need to think it further at some point and implement it properly.
+ var isFullScreen = document.fullScreen ||
+ document.mozFullScreen ||
+ document.webkitIsFullScreen;
+ if (isFullScreen)
+ videoSpaceHeight = window.innerHeight;
+
+ var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
+ var verticalIndent = (videoSpaceHeight - videoHeight) / 2;
+
+ return [horizontalIndent, verticalIndent];
+}
+
+/**
+ * Returns an array of the video horizontal and vertical indents.
+ * Centers horizontally and top aligns vertically.
+ *
+ * @return an array with 2 elements, the horizontal indent and the vertical
+ * indent
+ */
+function getDesktopVideoPosition(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight) {
+
+ var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
+
+ var verticalIndent = 0;// Top aligned
+
+ return [horizontalIndent, verticalIndent];
+}
+
+
+/**
+ * Returns an array of the video dimensions, so that it covers the screen.
+ * It leaves no empty areas, but some parts of the video might not be visible.
+ *
+ * @return an array with 2 elements, the video width and the video height
+ */
+function getCameraVideoSize(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight) {
+ if (!videoWidth)
+ videoWidth = currentVideoWidth;
+ if (!videoHeight)
+ videoHeight = currentVideoHeight;
+
+ var aspectRatio = videoWidth / videoHeight;
+
+ var availableWidth = Math.max(videoWidth, videoSpaceWidth);
+ var availableHeight = Math.max(videoHeight, videoSpaceHeight);
+
+ if (availableWidth / aspectRatio < videoSpaceHeight) {
+ availableHeight = videoSpaceHeight;
+ availableWidth = availableHeight * aspectRatio;
+ }
+
+ if (availableHeight * aspectRatio < videoSpaceWidth) {
+ availableWidth = videoSpaceWidth;
+ availableHeight = availableWidth / aspectRatio;
+ }
+
+ return [availableWidth, availableHeight];
+}
+
+/**
+ * Sets the display name for the given video span id.
+ */
+function setDisplayName(videoSpanId, displayName, key) {
+ var nameSpan = $('#' + videoSpanId + '>span.displayname');
+ var defaultLocalDisplayName = APP.translation.generateTranslatonHTML(
+ interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
+
+ // If we already have a display name for this video.
+ if (nameSpan.length > 0) {
+ var nameSpanElement = nameSpan.get(0);
+
+ if (nameSpanElement.id === 'localDisplayName' &&
+ $('#localDisplayName').text() !== displayName) {
+ if (displayName && displayName.length > 0)
+ {
+ var meHTML = APP.translation.generateTranslatonHTML("me");
+ $('#localDisplayName').html(displayName + ' (' + meHTML + ')');
+ }
+ else
+ $('#localDisplayName').html(defaultLocalDisplayName);
+ } else {
+ if (displayName && displayName.length > 0)
+ {
+ $('#' + videoSpanId + '_name').html(displayName);
+ }
+ else if (key && key.length > 0)
+ {
+ var nameHtml = APP.translation.generateTranslatonHTML(key);
+ $('#' + videoSpanId + '_name').html(nameHtml);
+ }
+ else
+ $('#' + videoSpanId + '_name').text(
+ interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
+ }
+ } else {
+ var editButton = null;
+
+ nameSpan = document.createElement('span');
+ nameSpan.className = 'displayname';
+ $('#' + videoSpanId)[0].appendChild(nameSpan);
+
+ if (videoSpanId === 'localVideoContainer') {
+ editButton = createEditDisplayNameButton();
+ if (displayName && displayName.length > 0) {
+ var meHTML = APP.translation.generateTranslatonHTML("me");
+ nameSpan.innerHTML = displayName + meHTML;
+ }
+ else
+ nameSpan.innerHTML = defaultLocalDisplayName;
+ }
+ else {
+ if (displayName && displayName.length > 0) {
+
+ nameSpan.innerText = displayName;
+ }
+ else
+ nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
+ }
+
+
+ if (!editButton) {
+ nameSpan.id = videoSpanId + '_name';
+ } else {
+ nameSpan.id = 'localDisplayName';
+ $('#' + videoSpanId)[0].appendChild(editButton);
+ //translates popover of edit button
+ APP.translation.translateElement($("a.displayname"));
+
+ var editableText = document.createElement('input');
+ editableText.className = 'displayname';
+ editableText.type = 'text';
+ editableText.id = 'editDisplayName';
+
+ if (displayName && displayName.length) {
+ editableText.value
+ = displayName;
+ }
+
+ var defaultNickname = APP.translation.translateString(
+ "defaultNickname", {name: "Jane Pink"});
+ editableText.setAttribute('style', 'display:none;');
+ editableText.setAttribute('data-18n',
+ '[placeholder]defaultNickname');
+ editableText.setAttribute("data-i18n-options",
+ JSON.stringify({name: "Jane Pink"}));
+ editableText.setAttribute("placeholder", defaultNickname);
+
+ $('#' + videoSpanId)[0].appendChild(editableText);
+
+ $('#localVideoContainer .displayname')
+ .bind("click", function (e) {
+
+ e.preventDefault();
+ e.stopPropagation();
+ $('#localDisplayName').hide();
+ $('#editDisplayName').show();
+ $('#editDisplayName').focus();
+ $('#editDisplayName').select();
+
+ $('#editDisplayName').one("focusout", function (e) {
+ VideoLayout.inputDisplayNameHandler(this.value);
+ });
+
+ $('#editDisplayName').on('keydown', function (e) {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ VideoLayout.inputDisplayNameHandler(this.value);
+ }
+ });
+ });
+ }
+ }
+}
+
+/**
+ * Gets the selector of video thumbnail container for the user identified by
+ * given userJid
+ * @param resourceJid user's Jid for whom we want to get the video container.
+ */
+function getParticipantContainer(resourceJid)
+{
+ if (!resourceJid)
+ return null;
+
+ if (resourceJid === APP.xmpp.myResource())
+ return $("#localVideoContainer");
+ else
+ return $("#participant_" + resourceJid);
+}
+
+/**
+ * Sets the size and position of the given video element.
+ *
+ * @param video the video element to position
+ * @param width the desired video width
+ * @param height the desired video height
+ * @param horizontalIndent the left and right indent
+ * @param verticalIndent the top and bottom indent
+ */
+function positionVideo(video,
+ width,
+ height,
+ horizontalIndent,
+ verticalIndent) {
+ video.width(width);
+ video.height(height);
+ video.css({ top: verticalIndent + 'px',
+ bottom: verticalIndent + 'px',
+ left: horizontalIndent + 'px',
+ right: horizontalIndent + 'px'});
+}
+
+/**
+ * Adds the remote video menu element for the given jid in the
+ * given parentElement .
+ *
+ * @param jid the jid indicating the video for which we're adding a menu.
+ * @param parentElement the parent element where this menu will be added
+ */
+function addRemoteVideoMenu(jid, parentElement) {
+ var spanElement = document.createElement('span');
+ spanElement.className = 'remotevideomenu';
+
+ parentElement.appendChild(spanElement);
+
+ var menuElement = document.createElement('i');
+ menuElement.className = 'fa fa-angle-down';
+ menuElement.title = 'Remote user controls';
+ spanElement.appendChild(menuElement);
+
+//
+
+ var popupmenuElement = document.createElement('ul');
+ popupmenuElement.className = 'popupmenu';
+ popupmenuElement.id
+ = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
+ spanElement.appendChild(popupmenuElement);
+
+ var muteMenuItem = document.createElement('li');
+ var muteLinkItem = document.createElement('a');
+
+ var mutedIndicator = " ";
+
+ if (!mutedAudios[jid]) {
+ muteLinkItem.innerHTML = mutedIndicator +
+ "
";
+ muteLinkItem.className = 'mutelink';
+ }
+ else {
+ muteLinkItem.innerHTML = mutedIndicator +
+ "
";
+ muteLinkItem.className = 'mutelink disabled';
+ }
+
+ muteLinkItem.onclick = function(){
+ if ($(this).attr('disabled') != undefined) {
+ event.preventDefault();
+ }
+ var isMute = mutedAudios[jid] == true;
+ APP.xmpp.setMute(jid, !isMute);
+
+ popupmenuElement.setAttribute('style', 'display:none;');
+
+ if (isMute) {
+ this.innerHTML = mutedIndicator +
+ "
";
+ this.className = 'mutelink disabled';
+ }
+ else {
+ this.innerHTML = mutedIndicator +
+ "
";
+ this.className = 'mutelink';
+ }
+ };
+
+ muteMenuItem.appendChild(muteLinkItem);
+ popupmenuElement.appendChild(muteMenuItem);
+
+ var ejectIndicator = " ";
+
+ var ejectMenuItem = document.createElement('li');
+ var ejectLinkItem = document.createElement('a');
+ var ejectText = "
";
+ ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
+ ejectLinkItem.onclick = function(){
+ APP.xmpp.eject(jid);
+ popupmenuElement.setAttribute('style', 'display:none;');
+ };
+
+ ejectMenuItem.appendChild(ejectLinkItem);
+ popupmenuElement.appendChild(ejectMenuItem);
+
+ var paddingSpan = document.createElement('span');
+ paddingSpan.className = 'popupmenuPadding';
+ popupmenuElement.appendChild(paddingSpan);
+ APP.translation.translateElement($("#" + popupmenuElement.id + " > li > a > div"));
+}
+
+/**
+ * Removes remote video menu element from video element identified by
+ * given videoElementId .
+ *
+ * @param videoElementId the id of local or remote video element.
+ */
+function removeRemoteVideoMenu(videoElementId) {
+ var menuSpan = $('#' + videoElementId + '>span.remotevideomenu');
+ if (menuSpan.length) {
+ menuSpan.remove();
+ }
+}
+
+/**
+ * Updates the data for the indicator
+ * @param id the id of the indicator
+ * @param percent the percent for connection quality
+ * @param object the data
+ */
+function updateStatsIndicator(id, percent, object) {
+ if(VideoLayout.connectionIndicators[id])
+ VideoLayout.connectionIndicators[id].updateConnectionQuality(percent, object);
+}
+
+
+/**
+ * Returns an array of the video dimensions, so that it keeps it's aspect
+ * ratio and fits available area with it's larger dimension. This method
+ * ensures that whole video will be visible and can leave empty areas.
+ *
+ * @return an array with 2 elements, the video width and the video height
+ */
+function getDesktopVideoSize(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight) {
+ if (!videoWidth)
+ videoWidth = currentVideoWidth;
+ if (!videoHeight)
+ videoHeight = currentVideoHeight;
+
+ var aspectRatio = videoWidth / videoHeight;
+
+ var availableWidth = Math.max(videoWidth, videoSpaceWidth);
+ var availableHeight = Math.max(videoHeight, videoSpaceHeight);
+
+ videoSpaceHeight -= $('#remoteVideos').outerHeight();
+
+ if (availableWidth / aspectRatio >= videoSpaceHeight)
+ {
+ availableHeight = videoSpaceHeight;
+ availableWidth = availableHeight * aspectRatio;
+ }
+
+ if (availableHeight * aspectRatio >= videoSpaceWidth)
+ {
+ availableWidth = videoSpaceWidth;
+ availableHeight = availableWidth / aspectRatio;
+ }
+
+ return [availableWidth, availableHeight];
+}
+
+/**
+ * Creates the edit display name button.
+ *
+ * @returns the edit button
+ */
+function createEditDisplayNameButton() {
+ var editButton = document.createElement('a');
+ editButton.className = 'displayname';
+ UIUtil.setTooltip(editButton,
+ "videothumbnail.editnickname",
+ "top");
+ editButton.innerHTML = ' ';
+
+ return editButton;
+}
+
+/**
+ * Creates the element indicating the moderator(owner) of the conference.
+ *
+ * @param parentElement the parent element where the owner indicator will
+ * be added
+ */
+function createModeratorIndicatorElement(parentElement) {
+ var moderatorIndicator = document.createElement('i');
+ moderatorIndicator.className = 'fa fa-star';
+ parentElement.appendChild(moderatorIndicator);
+
+ UIUtil.setTooltip(parentElement,
+ "videothumbnail.moderator",
+ "top");
+}
+
+
+var VideoLayout = (function (my) {
+ my.connectionIndicators = {};
+
+ // By default we use camera
+ my.getVideoSize = getCameraVideoSize;
+ my.getVideoPosition = getCameraVideoPosition;
+
+ my.init = function (emitter) {
+ // Listen for large video size updates
+ document.getElementById('largeVideo')
+ .addEventListener('loadedmetadata', function (e) {
+ currentVideoWidth = this.videoWidth;
+ currentVideoHeight = this.videoHeight;
+ VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight);
+ });
+ eventEmitter = emitter;
+ };
+
+ my.isInLastN = function(resource) {
+ return lastNCount < 0 // lastN is disabled, return true
+ || (lastNCount > 0 && lastNEndpointsCache.length == 0) // lastNEndpoints cache not built yet, return true
+ || (lastNEndpointsCache && lastNEndpointsCache.indexOf(resource) !== -1);
+ };
+
+ my.changeLocalStream = function (stream) {
+ VideoLayout.changeLocalVideo(stream);
+ };
+
+ my.changeLocalAudio = function(stream) {
+ APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
+ document.getElementById('localAudio').autoplay = true;
+ document.getElementById('localAudio').volume = 0;
+ if (preMuted) {
+ if(!APP.UI.setAudioMuted(true))
+ {
+ preMuted = mute;
+ }
+ preMuted = false;
+ }
+ };
+
+ my.changeLocalVideo = function(stream) {
+ var flipX = true;
+ if(stream.videoType == "screen")
+ flipX = false;
+ var localVideo = document.createElement('video');
+ localVideo.id = 'localVideo_' +
+ APP.RTC.getStreamID(stream.getOriginalStream());
+ localVideo.autoplay = true;
+ localVideo.volume = 0; // is it required if audio is separated ?
+ localVideo.oncontextmenu = function () { return false; };
+
+ var localVideoContainer = document.getElementById('localVideoWrapper');
+ localVideoContainer.appendChild(localVideo);
+
+ // Set default display name.
+ setDisplayName('localVideoContainer');
+
+ if(!VideoLayout.connectionIndicators["localVideoContainer"]) {
+ VideoLayout.connectionIndicators["localVideoContainer"]
+ = new ConnectionIndicator($("#localVideoContainer")[0], null, VideoLayout);
+ }
+
+ AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
+
+ var localVideoSelector = $('#' + localVideo.id);
+
+ function localVideoClick(event) {
+ event.stopPropagation();
+ VideoLayout.handleVideoThumbClicked(
+ APP.RTC.getVideoSrc(localVideo),
+ false,
+ APP.xmpp.myResource());
+ }
+ // Add click handler to both video and video wrapper elements in case
+ // there's no video.
+ localVideoSelector.click(localVideoClick);
+ $('#localVideoContainer').click(localVideoClick);
+
+ // Add hover handler
+ $('#localVideoContainer').hover(
+ function() {
+ VideoLayout.showDisplayName('localVideoContainer', true);
+ },
+ function() {
+ if (!VideoLayout.isLargeVideoVisible()
+ || APP.RTC.getVideoSrc(localVideo) !== APP.RTC.getVideoSrc($('#largeVideo')[0]))
+ VideoLayout.showDisplayName('localVideoContainer', false);
+ }
+ );
+ // Add stream ended handler
+ stream.getOriginalStream().onended = function () {
+ localVideoContainer.removeChild(localVideo);
+ VideoLayout.updateRemovedVideo(APP.RTC.getVideoSrc(localVideo));
+ };
+ // Flip video x axis if needed
+ flipXLocalVideo = flipX;
+ if (flipX) {
+ localVideoSelector.addClass("flipVideoX");
+ }
+ // Attach WebRTC stream
+ var videoStream = APP.simulcast.getLocalVideoStream();
+ APP.RTC.attachMediaStream(localVideoSelector, videoStream);
+
+ localVideoSrc = APP.RTC.getVideoSrc(localVideo);
+
+ var myResourceJid = APP.xmpp.myResource();
+
+ VideoLayout.updateLargeVideo(localVideoSrc, 0,
+ myResourceJid);
+
+ };
+
+ /**
+ * Checks if removed video is currently displayed and tries to display
+ * another one instead.
+ * @param removedVideoSrc src stream identifier of the video.
+ */
+ my.updateRemovedVideo = function(removedVideoSrc) {
+ if (removedVideoSrc === APP.RTC.getVideoSrc($('#largeVideo')[0])) {
+ // this is currently displayed as large
+ // pick the last visible video in the row
+ // if nobody else is left, this picks the local video
+ var pick
+ = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
+ .get(0);
+
+ if (!pick) {
+ console.info("Last visible video no longer exists");
+ pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
+
+ if (!pick || !APP.RTC.getVideoSrc(pick)) {
+ // Try local video
+ console.info("Fallback to local video...");
+ pick = $('#remoteVideos>span>span>video').get(0);
+ }
+ }
+
+ // mute if localvideo
+ if (pick) {
+ var container = pick.parentNode;
+ var jid = null;
+ if(container)
+ {
+ if(container.id == "localVideoWrapper")
+ {
+ jid = APP.xmpp.myResource();
+ }
+ else
+ {
+ jid = VideoLayout.getPeerContainerResourceJid(container);
+ }
+ }
+
+ VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(pick), pick.volume, jid);
+ } else {
+ console.warn("Failed to elect large video");
+ }
+ }
+ };
+
+ my.onRemoteStreamAdded = function (stream) {
+ var container;
+ var remotes = document.getElementById('remoteVideos');
+
+ if (stream.peerjid) {
+ VideoLayout.ensurePeerContainerExists(stream.peerjid);
+
+ container = document.getElementById(
+ 'participant_' + Strophe.getResourceFromJid(stream.peerjid));
+ } else {
+ var id = stream.getOriginalStream().id;
+ if (id !== 'mixedmslabel'
+ // FIXME: default stream is added always with new focus
+ // (to be investigated)
+ && id !== 'default') {
+ console.error('can not associate stream',
+ id,
+ 'with a participant');
+ // We don't want to add it here since it will cause troubles
+ return;
+ }
+ // FIXME: for the mixed ms we dont need a video -- currently
+ container = document.createElement('span');
+ container.id = 'mixedstream';
+ container.className = 'videocontainer';
+ remotes.appendChild(container);
+ UIUtil.playSoundNotification('userJoined');
+ }
+
+ if (container) {
+ VideoLayout.addRemoteStreamElement( container,
+ stream.sid,
+ stream.getOriginalStream(),
+ stream.peerjid,
+ stream.ssrc);
+ }
+ }
+
+ my.getLargeVideoState = function () {
+ return largeVideoState;
+ };
+
+ /**
+ * Updates the large video with the given new video source.
+ */
+ my.updateLargeVideo = function(newSrc, vol, resourceJid) {
+ console.log('hover in', newSrc);
+
+ if (APP.RTC.getVideoSrc($('#largeVideo')[0]) !== newSrc) {
+
+ $('#activeSpeaker').css('visibility', 'hidden');
+ // Due to the simulcast the localVideoSrc may have changed when the
+ // fadeOut event triggers. In that case the getJidFromVideoSrc and
+ // isVideoSrcDesktop methods will not function correctly.
+ //
+ // Also, again due to the simulcast, the updateLargeVideo method can
+ // be called multiple times almost simultaneously. Therefore, we
+ // store the state here and update only once.
+
+ largeVideoState.newSrc = newSrc;
+ largeVideoState.isVisible = $('#largeVideo').is(':visible');
+ largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop(
+ APP.xmpp.findJidFromResource(resourceJid));
+
+ if(largeVideoState.userResourceJid) {
+ largeVideoState.oldResourceJid = largeVideoState.userResourceJid;
+ } else {
+ largeVideoState.oldResourceJid = null;
+ }
+ largeVideoState.userResourceJid = resourceJid;
+
+ // Screen stream is already rotated
+ largeVideoState.flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
+
+ var userChanged = false;
+ if (largeVideoState.oldResourceJid !== largeVideoState.userResourceJid) {
+ userChanged = true;
+ // we want the notification to trigger even if userJid is undefined,
+ // or null.
+ eventEmitter.emit(UIEvents.SELECTED_ENDPOINT,
+ largeVideoState.userResourceJid);
+ }
+
+ if (!largeVideoState.updateInProgress) {
+ largeVideoState.updateInProgress = true;
+
+ var doUpdate = function () {
+
+ Avatar.updateActiveSpeakerAvatarSrc(
+ APP.xmpp.findJidFromResource(
+ largeVideoState.userResourceJid));
+
+ if (!userChanged && largeVideoState.preload &&
+ largeVideoState.preload !== null &&
+ APP.RTC.getVideoSrc($(largeVideoState.preload)[0]) === newSrc)
+ {
+
+ console.info('Switching to preloaded video');
+ var attributes = $('#largeVideo').prop("attributes");
+
+ // loop through largeVideo attributes and apply them on
+ // preload.
+ $.each(attributes, function () {
+ if (this.name !== 'id' && this.name !== 'src') {
+ largeVideoState.preload.attr(this.name, this.value);
+ }
+ });
+
+ largeVideoState.preload.appendTo($('#largeVideoContainer'));
+ $('#largeVideo').attr('id', 'previousLargeVideo');
+ largeVideoState.preload.attr('id', 'largeVideo');
+ $('#previousLargeVideo').remove();
+
+ largeVideoState.preload.on('loadedmetadata', function (e) {
+ currentVideoWidth = this.videoWidth;
+ currentVideoHeight = this.videoHeight;
+ VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight);
+ });
+ largeVideoState.preload = null;
+ largeVideoState.preload_ssrc = 0;
+ } else {
+ APP.RTC.setVideoSrc($('#largeVideo')[0], largeVideoState.newSrc);
+ }
+
+ var videoTransform = document.getElementById('largeVideo')
+ .style.webkitTransform;
+
+ if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') {
+ document.getElementById('largeVideo').style.webkitTransform
+ = "scaleX(-1)";
+ }
+ else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') {
+ document.getElementById('largeVideo').style.webkitTransform
+ = "none";
+ }
+
+ // Change the way we'll be measuring and positioning large video
+
+ VideoLayout.getVideoSize = largeVideoState.isDesktop
+ ? getDesktopVideoSize
+ : getCameraVideoSize;
+ VideoLayout.getVideoPosition = largeVideoState.isDesktop
+ ? getDesktopVideoPosition
+ : getCameraVideoPosition;
+
+
+ // Only if the large video is currently visible.
+ // Disable previous dominant speaker video.
+ if (largeVideoState.oldResourceJid) {
+ VideoLayout.enableDominantSpeaker(
+ largeVideoState.oldResourceJid,
+ false);
+ }
+
+ // Enable new dominant speaker in the remote videos section.
+ if (largeVideoState.userResourceJid) {
+ VideoLayout.enableDominantSpeaker(
+ largeVideoState.userResourceJid,
+ true);
+ }
+
+ if (userChanged && largeVideoState.isVisible) {
+ // using "this" should be ok because we're called
+ // from within the fadeOut event.
+ $(this).fadeIn(300);
+ }
+
+ if(userChanged) {
+ Avatar.showUserAvatar(
+ APP.xmpp.findJidFromResource(
+ largeVideoState.oldResourceJid));
+ }
+
+ largeVideoState.updateInProgress = false;
+ };
+
+ if (userChanged) {
+ $('#largeVideo').fadeOut(300, doUpdate);
+ } else {
+ doUpdate();
+ }
+ }
+ } else {
+ Avatar.showUserAvatar(
+ APP.xmpp.findJidFromResource(
+ largeVideoState.userResourceJid));
+ }
+
+ };
+
+ my.handleVideoThumbClicked = function(videoSrc,
+ noPinnedEndpointChangedEvent,
+ resourceJid) {
+ // Restore style for previously focused video
+ var oldContainer = null;
+ if(focusedVideoInfo) {
+ var focusResourceJid = focusedVideoInfo.resourceJid;
+ oldContainer = getParticipantContainer(focusResourceJid);
+ }
+
+ if (oldContainer) {
+ oldContainer.removeClass("videoContainerFocused");
+ }
+
+ // Unlock current focused.
+ if (focusedVideoInfo && focusedVideoInfo.src === videoSrc)
+ {
+ focusedVideoInfo = null;
+ var dominantSpeakerVideo = null;
+ // Enable the currently set dominant speaker.
+ if (currentDominantSpeaker) {
+ dominantSpeakerVideo
+ = $('#participant_' + currentDominantSpeaker + '>video')
+ .get(0);
+
+ if (dominantSpeakerVideo) {
+ VideoLayout.updateLargeVideo(
+ APP.RTC.getVideoSrc(dominantSpeakerVideo),
+ 1,
+ currentDominantSpeaker);
+ }
+ }
+
+ if (!noPinnedEndpointChangedEvent) {
+ eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
+ }
+ return;
+ }
+
+ // Lock new video
+ focusedVideoInfo = {
+ src: videoSrc,
+ resourceJid: resourceJid
+ };
+
+ // Update focused/pinned interface.
+ if (resourceJid)
+ {
+ var container = getParticipantContainer(resourceJid);
+ container.addClass("videoContainerFocused");
+
+ if (!noPinnedEndpointChangedEvent) {
+ eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
+ }
+ }
+
+ if ($('#largeVideo').attr('src') === videoSrc &&
+ VideoLayout.isLargeVideoOnTop()) {
+ return;
+ }
+
+ // Triggers a "video.selected" event. The "false" parameter indicates
+ // this isn't a prezi.
+ $(document).trigger("video.selected", [false]);
+
+ VideoLayout.updateLargeVideo(videoSrc, 1, resourceJid);
+
+ $('audio').each(function (idx, el) {
+ if (el.id.indexOf('mixedmslabel') !== -1) {
+ el.volume = 0;
+ el.volume = 1;
+ }
+ });
+ };
+
+ /**
+ * Positions the large video.
+ *
+ * @param videoWidth the stream video width
+ * @param videoHeight the stream video height
+ */
+ my.positionLarge = function (videoWidth, videoHeight) {
+ var videoSpaceWidth = $('#videospace').width();
+ var videoSpaceHeight = window.innerHeight;
+
+ var videoSize = VideoLayout.getVideoSize(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight);
+
+ var largeVideoWidth = videoSize[0];
+ var largeVideoHeight = videoSize[1];
+
+ var videoPosition = VideoLayout.getVideoPosition(largeVideoWidth,
+ largeVideoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight);
+
+ var horizontalIndent = videoPosition[0];
+ var verticalIndent = videoPosition[1];
+
+ positionVideo($('#largeVideo'),
+ largeVideoWidth,
+ largeVideoHeight,
+ horizontalIndent, verticalIndent);
+ };
+
+ /**
+ * Shows/hides the large video.
+ */
+ my.setLargeVideoVisible = function(isVisible) {
+ var resourceJid = largeVideoState.userResourceJid;
+
+ if (isVisible) {
+ $('#largeVideo').css({visibility: 'visible'});
+ $('.watermark').css({visibility: 'visible'});
+ VideoLayout.enableDominantSpeaker(resourceJid, true);
+ }
+ else {
+ $('#largeVideo').css({visibility: 'hidden'});
+ $('#activeSpeaker').css('visibility', 'hidden');
+ $('.watermark').css({visibility: 'hidden'});
+ VideoLayout.enableDominantSpeaker(resourceJid, false);
+ if(focusedVideoInfo) {
+ var focusResourceJid = focusedVideoInfo.resourceJid;
+ var oldContainer = getParticipantContainer(focusResourceJid);
+
+ if (oldContainer && oldContainer.length > 0) {
+ oldContainer.removeClass("videoContainerFocused");
+ }
+ focusedVideoInfo = null;
+ if(focusResourceJid) {
+ Avatar.showUserAvatar(
+ APP.xmpp.findJidFromResource(focusResourceJid));
+ }
+ }
+ }
+ };
+
+ /**
+ * Indicates if the large video is currently visible.
+ *
+ * @return true if visible, false - otherwise
+ */
+ my.isLargeVideoVisible = function() {
+ return $('#largeVideo').is(':visible');
+ };
+
+ my.isLargeVideoOnTop = function () {
+ var Etherpad = require("../etherpad/Etherpad");
+ var Prezi = require("../prezi/Prezi");
+ return !Prezi.isPresentationVisible() && !Etherpad.isVisible();
+ };
+
+ /**
+ * Checks if container for participant identified by given peerJid exists
+ * in the document and creates it eventually.
+ *
+ * @param peerJid peer Jid to check.
+ * @param userId user email or id for setting the avatar
+ *
+ * @return Returns true if the peer container exists,
+ * false - otherwise
+ */
+ my.ensurePeerContainerExists = function(peerJid, userId) {
+ ContactList.ensureAddContact(peerJid, userId);
+
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var videoSpanId = 'participant_' + resourceJid;
+
+ if (!$('#' + videoSpanId).length) {
+ var container =
+ VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId, userId);
+ Avatar.setUserAvatar(peerJid, userId);
+ // Set default display name.
+ setDisplayName(videoSpanId);
+
+ VideoLayout.connectionIndicators[videoSpanId] =
+ new ConnectionIndicator(container, peerJid, VideoLayout);
+
+ var nickfield = document.createElement('span');
+ nickfield.className = "nick";
+ nickfield.appendChild(document.createTextNode(resourceJid));
+ container.appendChild(nickfield);
+
+ // In case this is not currently in the last n we don't show it.
+ if (localLastNCount
+ && localLastNCount > 0
+ && $('#remoteVideos>span').length >= localLastNCount + 2) {
+ showPeerContainer(resourceJid, 'hide');
+ }
+ else
+ VideoLayout.resizeThumbnails();
+ }
+ };
+
+ my.addRemoteVideoContainer = function(peerJid, spanId) {
+ var container = document.createElement('span');
+ container.id = spanId;
+ container.className = 'videocontainer';
+ var remotes = document.getElementById('remoteVideos');
+ remotes.appendChild(container);
+ // If the peerJid is null then this video span couldn't be directly
+ // associated with a participant (this could happen in the case of prezi).
+ if (APP.xmpp.isModerator() && peerJid !== null)
+ addRemoteVideoMenu(peerJid, container);
+ AudioLevels.updateAudioLevelCanvas(peerJid, VideoLayout);
+
+ return container;
+ };
+
+ /**
+ * Creates an audio or video stream element.
+ */
+ my.createStreamElement = function (sid, stream) {
+ var isVideo = stream.getVideoTracks().length > 0;
+
+ var element = isVideo
+ ? document.createElement('video')
+ : document.createElement('audio');
+ var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_')
+ + sid + '_' + APP.RTC.getStreamID(stream);
+
+ element.id = id;
+ element.autoplay = true;
+ element.oncontextmenu = function () { return false; };
+
+ return element;
+ };
+
+ my.addRemoteStreamElement
+ = function (container, sid, stream, peerJid, thessrc) {
+ var newElementId = null;
+
+ var isVideo = stream.getVideoTracks().length > 0;
+
+ if (container) {
+ var streamElement = VideoLayout.createStreamElement(sid, stream);
+ newElementId = streamElement.id;
+
+ container.appendChild(streamElement);
+
+ var sel = $('#' + newElementId);
+ sel.hide();
+
+ // If the container is currently visible we attach the stream.
+ if (!isVideo
+ || (container.offsetParent !== null && isVideo)) {
+ var videoStream = APP.simulcast.getReceivingVideoStream(stream);
+ APP.RTC.attachMediaStream(sel, videoStream);
+
+ if (isVideo)
+ waitForRemoteVideo(sel, thessrc, stream, peerJid);
+ }
+
+ stream.onended = function () {
+ console.log('stream ended', this);
+
+ VideoLayout.removeRemoteStreamElement(
+ stream, isVideo, container);
+
+ // NOTE(gp) it seems that under certain circumstances, the
+ // onended event is not fired and thus the contact list is not
+ // updated.
+ //
+ // The onended event of a stream should be fired when the SSRCs
+ // corresponding to that stream are removed from the SDP; but
+ // this doesn't seem to always be the case, resulting in ghost
+ // contacts.
+ //
+ // In an attempt to fix the ghost contacts problem, I'm moving
+ // the removeContact() method call in app.js, inside the
+ // 'muc.left' event handler.
+
+ //if (peerJid)
+ // ContactList.removeContact(peerJid);
+ };
+
+ // Add click handler.
+ container.onclick = function (event) {
+ /*
+ * FIXME It turns out that videoThumb may not exist (if there is
+ * no actual video).
+ */
+ var videoThumb = $('#' + container.id + '>video').get(0);
+ if (videoThumb) {
+ VideoLayout.handleVideoThumbClicked(
+ APP.RTC.getVideoSrc(videoThumb),
+ false,
+ Strophe.getResourceFromJid(peerJid));
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ };
+
+ // Add hover handler
+ $(container).hover(
+ function() {
+ VideoLayout.showDisplayName(container.id, true);
+ },
+ function() {
+ var videoSrc = null;
+ if ($('#' + container.id + '>video')
+ && $('#' + container.id + '>video').length > 0) {
+ videoSrc = APP.RTC.getVideoSrc($('#' + container.id + '>video').get(0));
+ }
+
+ // If the video has been "pinned" by the user we want to
+ // keep the display name on place.
+ if (!VideoLayout.isLargeVideoVisible()
+ || videoSrc !== APP.RTC.getVideoSrc($('#largeVideo')[0]))
+ VideoLayout.showDisplayName(container.id, false);
+ }
+ );
+ }
+
+ return newElementId;
+ };
+
+ /**
+ * Removes the remote stream element corresponding to the given stream and
+ * parent container.
+ *
+ * @param stream the stream
+ * @param isVideo true if given stream is a video one.
+ * @param container
+ */
+ my.removeRemoteStreamElement = function (stream, isVideo, container) {
+ if (!container)
+ return;
+
+ var select = null;
+ var removedVideoSrc = null;
+ if (isVideo) {
+ select = $('#' + container.id + '>video');
+ removedVideoSrc = APP.RTC.getVideoSrc(select.get(0));
+ }
+ else
+ select = $('#' + container.id + '>audio');
+
+
+ // Mark video as removed to cancel waiting loop(if video is removed
+ // before has started)
+ select.removed = true;
+ select.remove();
+
+ var audioCount = $('#' + container.id + '>audio').length;
+ var videoCount = $('#' + container.id + '>video').length;
+
+ if (!audioCount && !videoCount) {
+ console.log("Remove whole user", container.id);
+ if(VideoLayout.connectionIndicators[container.id])
+ VideoLayout.connectionIndicators[container.id].remove();
+ // Remove whole container
+ container.remove();
+
+ UIUtil.playSoundNotification('userLeft');
+ VideoLayout.resizeThumbnails();
+ }
+
+ if (removedVideoSrc)
+ VideoLayout.updateRemovedVideo(removedVideoSrc);
+ };
+
+ /**
+ * Show/hide peer container for the given resourceJid.
+ */
+ function showPeerContainer(resourceJid, state) {
+ var peerContainer = $('#participant_' + resourceJid);
+
+ if (!peerContainer)
+ return;
+
+ var isHide = state === 'hide';
+ var resizeThumbnails = false;
+
+ if (!isHide) {
+ if (!peerContainer.is(':visible')) {
+ resizeThumbnails = true;
+ peerContainer.show();
+ }
+
+ var jid = APP.xmpp.findJidFromResource(resourceJid);
+ if (state == 'show')
+ {
+ // peerContainer.css('-webkit-filter', '');
+
+ Avatar.showUserAvatar(jid, false);
+ }
+ else // if (state == 'avatar')
+ {
+ // peerContainer.css('-webkit-filter', 'grayscale(100%)');
+ Avatar.showUserAvatar(jid, true);
+ }
+ }
+ else if (peerContainer.is(':visible') && isHide)
+ {
+ resizeThumbnails = true;
+ peerContainer.hide();
+ if(VideoLayout.connectionIndicators['participant_' + resourceJid])
+ VideoLayout.connectionIndicators['participant_' + resourceJid].hide();
+ }
+
+ if (resizeThumbnails) {
+ VideoLayout.resizeThumbnails();
+ }
+
+ // We want to be able to pin a participant from the contact list, even
+ // if he's not in the lastN set!
+ // ContactList.setClickable(resourceJid, !isHide);
+
+ };
+
+ my.inputDisplayNameHandler = function (name) {
+ NicknameHandler.setNickname(name);
+
+ if (!$('#localDisplayName').is(":visible")) {
+ if (NicknameHandler.getNickname())
+ {
+ var meHTML = APP.translation.generateTranslatonHTML("me");
+ $('#localDisplayName').html(NicknameHandler.getNickname() + " (" + meHTML + ")");
+ }
+ else
+ {
+ var defaultHTML = APP.translation.generateTranslatonHTML(
+ interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
+ $('#localDisplayName')
+ .html(defaultHTML);
+ }
+ $('#localDisplayName').show();
+ }
+
+ $('#editDisplayName').hide();
+ };
+
+ /**
+ * Shows/hides the display name on the remote video.
+ * @param videoSpanId the identifier of the video span element
+ * @param isShow indicates if the display name should be shown or hidden
+ */
+ my.showDisplayName = function(videoSpanId, isShow) {
+ var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
+ if (isShow) {
+ if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
+ nameSpan.setAttribute("style", "display:inline-block;");
+ }
+ else {
+ if (nameSpan)
+ nameSpan.setAttribute("style", "display:none;");
+ }
+ };
+
+ /**
+ * Shows the presence status message for the given video.
+ */
+ my.setPresenceStatus = function (videoSpanId, statusMsg) {
+
+ if (!$('#' + videoSpanId).length) {
+ // No container
+ return;
+ }
+
+ var statusSpan = $('#' + videoSpanId + '>span.status');
+ if (!statusSpan.length) {
+ //Add status span
+ statusSpan = document.createElement('span');
+ statusSpan.className = 'status';
+ statusSpan.id = videoSpanId + '_status';
+ $('#' + videoSpanId)[0].appendChild(statusSpan);
+
+ statusSpan = $('#' + videoSpanId + '>span.status');
+ }
+
+ // Display status
+ if (statusMsg && statusMsg.length) {
+ $('#' + videoSpanId + '_status').text(statusMsg);
+ statusSpan.get(0).setAttribute("style", "display:inline-block;");
+ }
+ else {
+ // Hide
+ statusSpan.get(0).setAttribute("style", "display:none;");
+ }
+ };
+
+ /**
+ * Shows a visual indicator for the moderator of the conference.
+ */
+ my.showModeratorIndicator = function () {
+
+ var isModerator = APP.xmpp.isModerator();
+ if (isModerator) {
+ var indicatorSpan = $('#localVideoContainer .focusindicator');
+
+ if (indicatorSpan.children().length === 0)
+ {
+ createModeratorIndicatorElement(indicatorSpan[0]);
+ //translates text in focus indicator
+ APP.translation.translateElement($('#localVideoContainer .focusindicator'));
+ }
+ }
+
+ var members = APP.xmpp.getMembers();
+
+ Object.keys(members).forEach(function (jid) {
+
+ if (Strophe.getResourceFromJid(jid) === 'focus') {
+ // Skip server side focus
+ return;
+ }
+
+ var resourceJid = Strophe.getResourceFromJid(jid);
+ var videoSpanId = 'participant_' + resourceJid;
+ var videoContainer = document.getElementById(videoSpanId);
+
+ if (!videoContainer) {
+ console.error("No video container for " + jid);
+ return;
+ }
+
+ var member = members[jid];
+
+ if (member.role === 'moderator') {
+ // Remove menu if peer is moderator
+ var menuSpan = $('#' + videoSpanId + '>span.remotevideomenu');
+ if (menuSpan.length) {
+ removeRemoteVideoMenu(videoSpanId);
+ }
+ // Show moderator indicator
+ var indicatorSpan
+ = $('#' + videoSpanId + ' .focusindicator');
+
+ if (!indicatorSpan || indicatorSpan.length === 0) {
+ indicatorSpan = document.createElement('span');
+ indicatorSpan.className = 'focusindicator';
+
+ videoContainer.appendChild(indicatorSpan);
+
+ createModeratorIndicatorElement(indicatorSpan);
+ //translates text in focus indicators
+ APP.translation.translateElement($('#' + videoSpanId + ' .focusindicator'));
+ }
+ } else if (isModerator) {
+ // We are moderator, but user is not - add menu
+ if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
+ addRemoteVideoMenu(
+ jid,
+ document.getElementById('participant_' + resourceJid));
+ }
+ }
+ });
+ };
+
+ /**
+ * Shows video muted indicator over small videos.
+ */
+ my.showVideoIndicator = function(videoSpanId, isMuted) {
+ var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
+
+ if (isMuted === 'false') {
+ if (videoMutedSpan.length > 0) {
+ videoMutedSpan.remove();
+ }
+ }
+ else {
+ if(videoMutedSpan.length == 0) {
+ videoMutedSpan = document.createElement('span');
+ videoMutedSpan.className = 'videoMuted';
+
+ $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
+
+ var mutedIndicator = document.createElement('i');
+ mutedIndicator.className = 'icon-camera-disabled';
+ UIUtil.setTooltip(mutedIndicator,
+ "videothumbnail.videomute",
+ "top");
+ videoMutedSpan.appendChild(mutedIndicator);
+ //translate texts for muted indicator
+ APP.translation.translateElement($('#' + videoSpanId + " > span > i"));
+ }
+
+ VideoLayout.updateMutePosition(videoSpanId);
+
+ }
+ };
+
+ my.updateMutePosition = function (videoSpanId) {
+ var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
+ var connectionIndicator = $('#' + videoSpanId + '>div.connectionindicator');
+ var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
+ if(connectionIndicator.length > 0
+ && connectionIndicator[0].style.display != "none") {
+ audioMutedSpan.css({right: "23px"});
+ videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
+ }
+ else
+ {
+ audioMutedSpan.css({right: "0px"});
+ videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
+ }
+ }
+ /**
+ * Shows audio muted indicator over small videos.
+ * @param {string} isMuted
+ */
+ my.showAudioIndicator = function(videoSpanId, isMuted) {
+ var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
+
+ if (isMuted === 'false') {
+ if (audioMutedSpan.length > 0) {
+ audioMutedSpan.popover('hide');
+ audioMutedSpan.remove();
+ }
+ }
+ else {
+ if(audioMutedSpan.length == 0 ) {
+ audioMutedSpan = document.createElement('span');
+ audioMutedSpan.className = 'audioMuted';
+ UIUtil.setTooltip(audioMutedSpan,
+ "videothumbnail.mute",
+ "top");
+
+ $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
+ APP.translation.translateElement($('#' + videoSpanId + " > span"));
+ var mutedIndicator = document.createElement('i');
+ mutedIndicator.className = 'icon-mic-disabled';
+ audioMutedSpan.appendChild(mutedIndicator);
+
+ }
+ VideoLayout.updateMutePosition(videoSpanId);
+ }
+ };
+
+ /*
+ * Shows or hides the audio muted indicator over the local thumbnail video.
+ * @param {boolean} isMuted
+ */
+ my.showLocalAudioIndicator = function(isMuted) {
+ VideoLayout.showAudioIndicator('localVideoContainer', isMuted.toString());
+ };
+
+ /**
+ * Resizes the large video container.
+ */
+ my.resizeLargeVideoContainer = function () {
+ Chat.resizeChat();
+ var availableHeight = window.innerHeight;
+ var availableWidth = UIUtil.getAvailableVideoWidth();
+
+ if (availableWidth < 0 || availableHeight < 0) return;
+
+ $('#videospace').width(availableWidth);
+ $('#videospace').height(availableHeight);
+ $('#largeVideoContainer').width(availableWidth);
+ $('#largeVideoContainer').height(availableHeight);
+
+ var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
+ var top = availableHeight / 2 - avatarSize / 4 * 3;
+ $('#activeSpeaker').css('top', top);
+
+ VideoLayout.resizeThumbnails();
+ };
+
+ /**
+ * Resizes thumbnails.
+ */
+ my.resizeThumbnails = function() {
+ var videoSpaceWidth = $('#remoteVideos').width();
+
+ var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
+ var width = thumbnailSize[0];
+ var height = thumbnailSize[1];
+
+ // size videos so that while keeping AR and max height, we have a
+ // nice fit
+ $('#remoteVideos').height(height);
+ $('#remoteVideos>span').width(width);
+ $('#remoteVideos>span').height(height);
+
+ $('.userAvatar').css('left', (width - height) / 2);
+
+
+
+ $(document).trigger("remotevideo.resized", [width, height]);
+ };
+
+ /**
+ * Enables the dominant speaker UI.
+ *
+ * @param resourceJid the jid indicating the video element to
+ * activate/deactivate
+ * @param isEnable indicates if the dominant speaker should be enabled or
+ * disabled
+ */
+ my.enableDominantSpeaker = function(resourceJid, isEnable) {
+
+ var videoSpanId = null;
+ var videoContainerId = null;
+ if (resourceJid
+ === APP.xmpp.myResource()) {
+ videoSpanId = 'localVideoWrapper';
+ videoContainerId = 'localVideoContainer';
+ }
+ else {
+ videoSpanId = 'participant_' + resourceJid;
+ videoContainerId = videoSpanId;
+ }
+
+ var displayName = resourceJid;
+ var nameSpan = $('#' + videoContainerId + '>span.displayname');
+ if (nameSpan.length > 0)
+ displayName = nameSpan.html();
+
+ console.log("UI enable dominant speaker",
+ displayName,
+ resourceJid,
+ isEnable);
+
+ videoSpan = document.getElementById(videoContainerId);
+
+ if (!videoSpan) {
+ return;
+ }
+
+ var video = $('#' + videoSpanId + '>video');
+
+ if (video && video.length > 0) {
+ if (isEnable) {
+ var isLargeVideoVisible = VideoLayout.isLargeVideoOnTop();
+ VideoLayout.showDisplayName(videoContainerId, isLargeVideoVisible);
+
+ if (!videoSpan.classList.contains("dominantspeaker"))
+ videoSpan.classList.add("dominantspeaker");
+ }
+ else {
+ VideoLayout.showDisplayName(videoContainerId, false);
+
+ if (videoSpan.classList.contains("dominantspeaker"))
+ videoSpan.classList.remove("dominantspeaker");
+ }
+
+ Avatar.showUserAvatar(
+ APP.xmpp.findJidFromResource(resourceJid));
+ }
+ };
+
+ /**
+ * Calculates the thumbnail size.
+ *
+ * @param videoSpaceWidth the width of the video space
+ */
+ my.calculateThumbnailSize = function (videoSpaceWidth) {
+ // Calculate the available height, which is the inner window height minus
+ // 39px for the header minus 2px for the delimiter lines on the top and
+ // bottom of the large video, minus the 36px space inside the remoteVideos
+ // container used for highlighting shadow.
+ var availableHeight = 100;
+
+ var numvids = $('#remoteVideos>span:visible').length;
+ if (localLastNCount && localLastNCount > 0) {
+ numvids = Math.min(localLastNCount + 1, numvids);
+ }
+
+ // Remove the 3px borders arround videos and border around the remote
+ // videos area and the 4 pixels between the local video and the others
+ //TODO: Find out where the 4 pixels come from and remove them
+ var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4;
+
+ var availableWidth = availableWinWidth / numvids;
+ var aspectRatio = 16.0 / 9.0;
+ var maxHeight = Math.min(160, availableHeight);
+ availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
+ if (availableHeight < availableWidth / aspectRatio) {
+ availableWidth = Math.floor(availableHeight * aspectRatio);
+ }
+
+ return [availableWidth, availableHeight];
+ };
+
+ /**
+ * Updates the remote video menu.
+ *
+ * @param jid the jid indicating the video for which we're adding a menu.
+ * @param isMuted indicates the current mute state
+ */
+ my.updateRemoteVideoMenu = function(jid, isMuted) {
+ var muteMenuItem
+ = $('#remote_popupmenu_'
+ + Strophe.getResourceFromJid(jid)
+ + '>li>a.mutelink');
+
+ var mutedIndicator = " ";
+
+ if (muteMenuItem.length) {
+ var muteLink = muteMenuItem.get(0);
+
+ if (isMuted === 'true') {
+ muteLink.innerHTML = mutedIndicator + ' Muted';
+ muteLink.className = 'mutelink disabled';
+ }
+ else {
+ muteLink.innerHTML = mutedIndicator + ' Mute';
+ muteLink.className = 'mutelink';
+ }
+ }
+ };
+
+ /**
+ * Returns the current dominant speaker resource jid.
+ */
+ my.getDominantSpeakerResourceJid = function () {
+ return currentDominantSpeaker;
+ };
+
+ /**
+ * Returns the corresponding resource jid to the given peer container
+ * DOM element.
+ *
+ * @return the corresponding resource jid to the given peer container
+ * DOM element
+ */
+ my.getPeerContainerResourceJid = function (containerElement) {
+ var i = containerElement.id.indexOf('participant_');
+
+ if (i >= 0)
+ return containerElement.id.substring(i + 12);
+ };
+
+ /**
+ * On contact list item clicked.
+ */
+ $(ContactList).bind('contactclicked', function(event, jid) {
+ if (!jid) {
+ return;
+ }
+
+ var resource = Strophe.getResourceFromJid(jid);
+ var videoContainer = $("#participant_" + resource);
+ if (videoContainer.length > 0) {
+ var videoThumb = $('video', videoContainer).get(0);
+ // It is not always the case that a videoThumb exists (if there is
+ // no actual video).
+ if (videoThumb) {
+ if (videoThumb.src && videoThumb.src != '') {
+
+ // We have a video src, great! Let's update the large video
+ // now.
+
+ VideoLayout.handleVideoThumbClicked(
+ videoThumb.src,
+ false,
+ Strophe.getResourceFromJid(jid));
+ } else {
+
+ // If we don't have a video src for jid, there's absolutely
+ // no point in calling handleVideoThumbClicked; Quite
+ // simply, it won't work because it needs an src to attach
+ // to the large video.
+ //
+ // Instead, we trigger the pinned endpoint changed event to
+ // let the bridge adjust its lastN set for myjid and store
+ // the pinned user in the lastNPickupJid variable to be
+ // picked up later by the lastN changed event handler.
+
+ lastNPickupJid = jid;
+ eventEmitter.emit(UIEvents.PINNED_ENDPOINT,
+ Strophe.getResourceFromJid(jid));
+ }
+ } else if (jid == APP.xmpp.myJid()) {
+ $("#localVideoContainer").click();
+ }
+ }
+ });
+
+ /**
+ * On audio muted event.
+ */
+ $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
+ /*
+ // FIXME: but focus can not mute in this case ? - check
+ if (jid === xmpp.myJid()) {
+
+ // The local mute indicator is controlled locally
+ return;
+ }*/
+ var videoSpanId = null;
+ if (jid === APP.xmpp.myJid()) {
+ videoSpanId = 'localVideoContainer';
+ } else {
+ VideoLayout.ensurePeerContainerExists(jid);
+ videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
+ }
+
+ mutedAudios[jid] = isMuted;
+
+ if (APP.xmpp.isModerator()) {
+ VideoLayout.updateRemoteVideoMenu(jid, isMuted);
+ }
+
+ if (videoSpanId)
+ VideoLayout.showAudioIndicator(videoSpanId, isMuted);
+ });
+
+ /**
+ * On video muted event.
+ */
+ $(document).bind('videomuted.muc', function (event, jid, value) {
+ var isMuted = (value === "true");
+ if(jid !== APP.xmpp.myJid() && !APP.RTC.muteRemoteVideoStream(jid, isMuted))
+ return;
+
+ Avatar.showUserAvatar(jid, isMuted);
+ var videoSpanId = null;
+ if (jid === APP.xmpp.myJid()) {
+ videoSpanId = 'localVideoContainer';
+ } else {
+ VideoLayout.ensurePeerContainerExists(jid);
+ videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
+ }
+
+ if (videoSpanId)
+ VideoLayout.showVideoIndicator(videoSpanId, value);
+ });
+
+ /**
+ * Display name changed.
+ */
+ my.onDisplayNameChanged =
+ function (jid, displayName, status) {
+ if (jid === 'localVideoContainer'
+ || jid === APP.xmpp.myJid()) {
+ setDisplayName('localVideoContainer',
+ displayName);
+ } else {
+ VideoLayout.ensurePeerContainerExists(jid);
+ setDisplayName(
+ 'participant_' + Strophe.getResourceFromJid(jid),
+ displayName,
+ status);
+ }
+
+ };
+
+ /**
+ * On dominant speaker changed event.
+ */
+ my.onDominantSpeakerChanged = function (resourceJid) {
+ // We ignore local user events.
+ if (resourceJid
+ === APP.xmpp.myResource())
+ return;
+
+ var members = APP.xmpp.getMembers();
+ // Update the current dominant speaker.
+ if (resourceJid !== currentDominantSpeaker) {
+ var oldSpeakerVideoSpanId = "participant_" + currentDominantSpeaker,
+ newSpeakerVideoSpanId = "participant_" + resourceJid;
+ var currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker);
+ var newJID = APP.xmpp.findJidFromResource(resourceJid);
+ if(currentDominantSpeaker && (!members || !members[currentJID] ||
+ !members[currentJID].displayName)) {
+ setDisplayName(oldSpeakerVideoSpanId, null);
+ }
+ if(resourceJid && (!members || !members[newJID] ||
+ !members[newJID].displayName)) {
+ setDisplayName(newSpeakerVideoSpanId, null,
+ interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME);
+ }
+ currentDominantSpeaker = resourceJid;
+ } else {
+ return;
+ }
+
+ // Obtain container for new dominant speaker.
+ var container = document.getElementById(
+ 'participant_' + resourceJid);
+
+ // Local video will not have container found, but that's ok
+ // since we don't want to switch to local video.
+ if (container && !focusedVideoInfo)
+ {
+ var video = container.getElementsByTagName("video");
+
+ // Update the large video if the video source is already available,
+ // otherwise wait for the "videoactive.jingle" event.
+ if (video.length && video[0].currentTime > 0)
+ VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(video[0]), resourceJid);
+ }
+ };
+
+ /**
+ * On last N change event.
+ *
+ * @param lastNEndpoints the list of last N endpoints
+ * @param endpointsEnteringLastN the list currently entering last N
+ * endpoints
+ */
+ my.onLastNEndpointsChanged = function ( lastNEndpoints,
+ endpointsEnteringLastN,
+ stream) {
+ if (lastNCount !== lastNEndpoints.length)
+ lastNCount = lastNEndpoints.length;
+
+ lastNEndpointsCache = lastNEndpoints;
+
+ // Say A, B, C, D, E, and F are in a conference and LastN = 3.
+ //
+ // If LastN drops to, say, 2, because of adaptivity, then E should see
+ // thumbnails for A, B and C. A and B are in E's server side LastN set,
+ // so E sees them. C is only in E's local LastN set.
+ //
+ // If F starts talking and LastN = 3, then E should see thumbnails for
+ // F, A, B. B gets "ejected" from E's server side LastN set, but it
+ // enters E's local LastN ejecting C.
+
+ // Increase the local LastN set size, if necessary.
+ if (lastNCount > localLastNCount) {
+ localLastNCount = lastNCount;
+ }
+
+ // Update the local LastN set preserving the order in which the
+ // endpoints appeared in the LastN/local LastN set.
+
+ var nextLocalLastNSet = lastNEndpoints.slice(0);
+ for (var i = 0; i < localLastNSet.length; i++) {
+ if (nextLocalLastNSet.length >= localLastNCount) {
+ break;
+ }
+
+ var resourceJid = localLastNSet[i];
+ if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
+ nextLocalLastNSet.push(resourceJid);
+ }
+ }
+
+ localLastNSet = nextLocalLastNSet;
+
+ var updateLargeVideo = false;
+
+ // Handle LastN/local LastN changes.
+ $('#remoteVideos>span').each(function( index, element ) {
+ var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
+
+ var isReceived = true;
+ if (resourceJid
+ && lastNEndpoints.indexOf(resourceJid) < 0
+ && localLastNSet.indexOf(resourceJid) < 0) {
+ console.log("Remove from last N", resourceJid);
+ showPeerContainer(resourceJid, 'hide');
+ isReceived = false;
+ } else if (resourceJid
+ && $('#participant_' + resourceJid).is(':visible')
+ && lastNEndpoints.indexOf(resourceJid) < 0
+ && localLastNSet.indexOf(resourceJid) >= 0) {
+ showPeerContainer(resourceJid, 'avatar');
+ isReceived = false;
+ }
+
+ if (!isReceived) {
+ // resourceJid has dropped out of the server side lastN set, so
+ // it is no longer being received. If resourceJid was being
+ // displayed in the large video we have to switch to another
+ // user.
+ var largeVideoResource = largeVideoState.userResourceJid;
+ if (!updateLargeVideo && resourceJid === largeVideoResource) {
+ updateLargeVideo = true;
+ }
+ }
+ });
+
+ if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
+ endpointsEnteringLastN = lastNEndpoints;
+
+ if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
+ endpointsEnteringLastN.forEach(function (resourceJid) {
+
+ var isVisible = $('#participant_' + resourceJid).is(':visible');
+ showPeerContainer(resourceJid, 'show');
+ if (!isVisible) {
+ console.log("Add to last N", resourceJid);
+
+ var jid = APP.xmpp.findJidFromResource(resourceJid);
+ var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
+ var sel = $('#participant_' + resourceJid + '>video');
+
+ var videoStream = APP.simulcast.getReceivingVideoStream(
+ mediaStream.stream);
+ APP.RTC.attachMediaStream(sel, videoStream);
+ if (lastNPickupJid == mediaStream.peerjid) {
+ // Clean up the lastN pickup jid.
+ lastNPickupJid = null;
+
+ // Don't fire the events again, they've already
+ // been fired in the contact list click handler.
+ VideoLayout.handleVideoThumbClicked(
+ $(sel).attr('src'),
+ false,
+ Strophe.getResourceFromJid(mediaStream.peerjid));
+
+ updateLargeVideo = false;
+ }
+ waitForRemoteVideo(sel, mediaStream.ssrc, mediaStream.stream, resourceJid);
+ }
+ })
+ }
+
+ // The endpoint that was being shown in the large video has dropped out
+ // of the lastN set and there was no lastN pickup jid. We need to update
+ // the large video now.
+
+ if (updateLargeVideo) {
+
+ var resource, container, src;
+ var myResource
+ = APP.xmpp.myResource();
+
+ // Find out which endpoint to show in the large video.
+ for (var i = 0; i < lastNEndpoints.length; i++) {
+ resource = lastNEndpoints[i];
+ if (!resource || resource === myResource)
+ continue;
+
+ container = $("#participant_" + resource);
+ if (container.length == 0)
+ continue;
+
+ src = $('video', container).attr('src');
+ if (!src)
+ continue;
+
+ // videoSrcToSsrc needs to be update for this call to succeed.
+ VideoLayout.updateLargeVideo(src);
+ break;
+
+ }
+ }
+ };
+
+ my.onSimulcastLayersChanging = function (endpointSimulcastLayers) {
+ endpointSimulcastLayers.forEach(function (esl) {
+
+ var resource = esl.endpoint;
+
+ // if lastN is enabled *and* the endpoint is *not* in the lastN set,
+ // then ignore the event (= do not preload anything).
+ //
+ // The bridge could probably stop sending this message if it's for
+ // an endpoint that's not in lastN.
+
+ if (lastNCount != -1
+ && (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
+ return;
+ }
+
+ var primarySSRC = esl.simulcastLayer.primarySSRC;
+
+ // Get session and stream from primary ssrc.
+ var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
+ var sid = res.sid;
+ var electedStream = res.stream;
+
+ if (sid && electedStream) {
+ var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
+
+ console.info([esl, primarySSRC, msid, sid, electedStream]);
+
+ var preload = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC)) == largeVideoState.userResourceJid);
+
+ if (preload) {
+ if (largeVideoState.preload)
+ {
+ $(largeVideoState.preload).remove();
+ }
+ console.info('Preloading remote video');
+ largeVideoState.preload = $(' ');
+ // ssrcs are unique in an rtp session
+ largeVideoState.preload_ssrc = primarySSRC;
+
+ APP.RTC.attachMediaStream(largeVideoState.preload, electedStream)
+ }
+
+ } else {
+ console.error('Could not find a stream or a session.', sid, electedStream);
+ }
+ });
+ };
+
+ /**
+ * On simulcast layers changed event.
+ */
+ my.onSimulcastLayersChanged = function (endpointSimulcastLayers) {
+ endpointSimulcastLayers.forEach(function (esl) {
+
+ var resource = esl.endpoint;
+
+ // if lastN is enabled *and* the endpoint is *not* in the lastN set,
+ // then ignore the event (= do not change large video/thumbnail
+ // SRCs).
+ //
+ // Note that even if we ignore the "changed" event in this event
+ // handler, the bridge must continue sending these events because
+ // the simulcast code in simulcast.js uses it to know what's going
+ // to be streamed by the bridge when/if the endpoint gets back into
+ // the lastN set.
+
+ if (lastNCount != -1
+ && (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
+ return;
+ }
+
+ var primarySSRC = esl.simulcastLayer.primarySSRC;
+
+ // Get session and stream from primary ssrc.
+ var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
+ var sid = res.sid;
+ var electedStream = res.stream;
+
+ if (sid && electedStream) {
+ var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
+
+ console.info('Switching simulcast substream.');
+ console.info([esl, primarySSRC, msid, sid, electedStream]);
+
+ var msidParts = msid.split(' ');
+ var selRemoteVideo = $(['#', 'remoteVideo_', sid, '_', msidParts[0]].join(''));
+
+ var updateLargeVideo = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC))
+ == largeVideoState.userResourceJid);
+ var updateFocusedVideoSrc = (focusedVideoInfo && focusedVideoInfo.src && focusedVideoInfo.src != '' &&
+ (APP.RTC.getVideoSrc(selRemoteVideo[0]) == focusedVideoInfo.src));
+
+ var electedStreamUrl;
+ if (largeVideoState.preload_ssrc == primarySSRC)
+ {
+ APP.RTC.setVideoSrc(selRemoteVideo[0], APP.RTC.getVideoSrc(largeVideoState.preload[0]));
+ }
+ else
+ {
+ if (largeVideoState.preload
+ && largeVideoState.preload != null) {
+ $(largeVideoState.preload).remove();
+ }
+
+ largeVideoState.preload_ssrc = 0;
+
+ APP.RTC.attachMediaStream(selRemoteVideo, electedStream);
+ }
+
+ var jid = APP.xmpp.getJidFromSSRC(primarySSRC);
+
+ if (updateLargeVideo) {
+ VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(selRemoteVideo[0]), null,
+ Strophe.getResourceFromJid(jid));
+ }
+
+ if (updateFocusedVideoSrc) {
+ focusedVideoInfo.src = APP.RTC.getVideoSrc(selRemoteVideo[0]);
+ }
+
+ var videoId;
+ if(resource == APP.xmpp.myResource())
+ {
+ videoId = "localVideoContainer";
+ }
+ else
+ {
+ videoId = "participant_" + resource;
+ }
+ var connectionIndicator = VideoLayout.connectionIndicators[videoId];
+ if(connectionIndicator)
+ connectionIndicator.updatePopoverData();
+
+ } else {
+ console.error('Could not find a stream or a sid.', sid, electedStream);
+ }
+ });
+ };
+
+ /**
+ * Updates local stats
+ * @param percent
+ * @param object
+ */
+ my.updateLocalConnectionStats = function (percent, object) {
+ var resolution = null;
+ if(object.resolution !== null)
+ {
+ resolution = object.resolution;
+ object.resolution = resolution[APP.xmpp.myJid()];
+ delete resolution[APP.xmpp.myJid()];
+ }
+ updateStatsIndicator("localVideoContainer", percent, object);
+ for(var jid in resolution)
+ {
+ if(resolution[jid] === null)
+ continue;
+ var id = 'participant_' + Strophe.getResourceFromJid(jid);
+ if(VideoLayout.connectionIndicators[id])
+ {
+ VideoLayout.connectionIndicators[id].updateResolution(resolution[jid]);
+ }
+ }
+
+ };
+
+ /**
+ * Updates remote stats.
+ * @param jid the jid associated with the stats
+ * @param percent the connection quality percent
+ * @param object the stats data
+ */
+ my.updateConnectionStats = function (jid, percent, object) {
+ var resourceJid = Strophe.getResourceFromJid(jid);
+
+ var videoSpanId = 'participant_' + resourceJid;
+ updateStatsIndicator(videoSpanId, percent, object);
+ };
+
+ /**
+ * Removes the connection
+ * @param jid
+ */
+ my.removeConnectionIndicator = function (jid) {
+ if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)])
+ VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].remove();
+ };
+
+ /**
+ * Hides the connection indicator
+ * @param jid
+ */
+ my.hideConnectionIndicator = function (jid) {
+ if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)])
+ VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].hide();
+ };
+
+ /**
+ * Hides all the indicators
+ */
+ my.onStatsStop = function () {
+ for(var indicator in VideoLayout.connectionIndicators)
+ {
+ VideoLayout.connectionIndicators[indicator].hideIndicator();
+ }
+ };
+
+ my.participantLeft = function (jid) {
+ // Unlock large video
+ if (focusedVideoInfo && focusedVideoInfo.jid === jid)
+ {
+ console.info("Focused video owner has left the conference");
+ focusedVideoInfo = null;
+ }
+ }
+
+ my.onVideoTypeChanged = function (jid) {
+ if(jid &&
+ Strophe.getResourceFromJid(jid) === largeVideoState.userResourceJid)
+ {
+ largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop(jid);
+ VideoLayout.getVideoSize = largeVideoState.isDesktop
+ ? getDesktopVideoSize
+ : getCameraVideoSize;
+ VideoLayout.getVideoPosition = largeVideoState.isDesktop
+ ? getDesktopVideoPosition
+ : getCameraVideoPosition;
+ VideoLayout.positionLarge(null, null);
+ }
+ }
+
+ return my;
+}(VideoLayout || {}));
+
module.exports = VideoLayout;
-},{"../../../service/RTC/MediaStreamTypes":87,"../../../service/UI/UIEvents":92,"../audio_levels/AudioLevels":9,"../avatar/Avatar":13,"../etherpad/Etherpad":14,"../prezi/Prezi":15,"../side_pannels/chat/Chat":18,"../side_pannels/contactlist/ContactList":22,"../util/NicknameHandler":29,"../util/UIUtil":30,"./ConnectionIndicator":31}],33:[function(require,module,exports){
-//var nouns = [
-//];
-var pluralNouns = [
- "Aliens", "Animals", "Antelopes", "Ants", "Apes", "Apples", "Baboons", "Bacteria", "Badgers", "Bananas", "Bats",
- "Bears", "Birds", "Bonobos", "Brides", "Bugs", "Bulls", "Butterflies", "Cheetahs",
- "Cherries", "Chicken", "Children", "Chimps", "Clowns", "Cows", "Creatures", "Dinosaurs", "Dogs", "Dolphins",
- "Donkeys", "Dragons", "Ducks", "Dwarfs", "Eagles", "Elephants", "Elves", "FAIL", "Fathers",
- "Fish", "Flowers", "Frogs", "Fruit", "Fungi", "Galaxies", "Geese", "Goats",
- "Gorillas", "Hedgehogs", "Hippos", "Horses", "Hunters", "Insects", "Kids", "Knights",
- "Lemons", "Lemurs", "Leopards", "LifeForms", "Lions", "Lizards", "Mice", "Monkeys", "Monsters",
- "Mushrooms", "Octopodes", "Oranges", "Orangutans", "Organisms", "Pants", "Parrots", "Penguins",
- "People", "Pigeons", "Pigs", "Pineapples", "Plants", "Potatoes", "Priests", "Rats", "Reptiles", "Reptilians",
- "Rhinos", "Seagulls", "Sheep", "Siblings", "Snakes", "Spaghetti", "Spiders", "Squid", "Squirrels",
- "Stars", "Students", "Teachers", "Tigers", "Tomatoes", "Trees", "Vampires", "Vegetables", "Viruses", "Vulcans",
- "Warewolves", "Weasels", "Whales", "Witches", "Wizards", "Wolves", "Workers", "Worms", "Zebras"
-];
-//var places = [
-//"Pub", "University", "Airport", "Library", "Mall", "Theater", "Stadium", "Office", "Show", "Gallows", "Beach",
-// "Cemetery", "Hospital", "Reception", "Restaurant", "Bar", "Church", "House", "School", "Square", "Village",
-// "Cinema", "Movies", "Party", "Restroom", "End", "Jail", "PostOffice", "Station", "Circus", "Gates", "Entrance",
-// "Bridge"
-//];
-var verbs = [
- "Abandon", "Adapt", "Advertise", "Answer", "Anticipate", "Appreciate",
- "Approach", "Argue", "Ask", "Bite", "Blossom", "Blush", "Breathe", "Breed", "Bribe", "Burn", "Calculate",
- "Clean", "Code", "Communicate", "Compute", "Confess", "Confiscate", "Conjugate", "Conjure", "Consume",
- "Contemplate", "Crawl", "Dance", "Delegate", "Devour", "Develop", "Differ", "Discuss",
- "Dissolve", "Drink", "Eat", "Elaborate", "Emancipate", "Estimate", "Expire", "Extinguish",
- "Extract", "FAIL", "Facilitate", "Fall", "Feed", "Finish", "Floss", "Fly", "Follow", "Fragment", "Freeze",
- "Gather", "Glow", "Grow", "Hex", "Hide", "Hug", "Hurry", "Improve", "Intersect", "Investigate", "Jinx",
- "Joke", "Jubilate", "Kiss", "Laugh", "Manage", "Meet", "Merge", "Move", "Object", "Observe", "Offer",
- "Paint", "Participate", "Party", "Perform", "Plan", "Pursue", "Pierce", "Play", "Postpone", "Pray", "Proclaim",
- "Question", "Read", "Reckon", "Rejoice", "Represent", "Resize", "Rhyme", "Scream", "Search", "Select", "Share", "Shoot",
- "Shout", "Signal", "Sing", "Skate", "Sleep", "Smile", "Smoke", "Solve", "Spell", "Steer", "Stink",
- "Substitute", "Swim", "Taste", "Teach", "Terminate", "Think", "Type", "Unite", "Vanish", "Worship"
-];
-var adverbs = [
- "Absently", "Accurately", "Accusingly", "Adorably", "AllTheTime", "Alone", "Always", "Amazingly", "Angrily",
- "Anxiously", "Anywhere", "Appallingly", "Apparently", "Articulately", "Astonishingly", "Badly", "Barely",
- "Beautifully", "Blindly", "Bravely", "Brightly", "Briskly", "Brutally", "Calmly", "Carefully", "Casually",
- "Cautiously", "Cleverly", "Constantly", "Correctly", "Crazily", "Curiously", "Cynically", "Daily",
- "Dangerously", "Deliberately", "Delicately", "Desperately", "Discreetly", "Eagerly", "Easily", "Euphoricly",
- "Evenly", "Everywhere", "Exactly", "Expectantly", "Extensively", "FAIL", "Ferociously", "Fiercely", "Finely",
- "Flatly", "Frequently", "Frighteningly", "Gently", "Gloriously", "Grimly", "Guiltily", "Happily",
- "Hard", "Hastily", "Heroically", "High", "Highly", "Hourly", "Humbly", "Hysterically", "Immensely",
- "Impartially", "Impolitely", "Indifferently", "Intensely", "Jealously", "Jovially", "Kindly", "Lazily",
- "Lightly", "Loudly", "Lovingly", "Loyally", "Magnificently", "Malevolently", "Merrily", "Mightily", "Miserably",
- "Mysteriously", "NOT", "Nervously", "Nicely", "Nowhere", "Objectively", "Obnoxiously", "Obsessively",
- "Obviously", "Often", "Painfully", "Patiently", "Playfully", "Politely", "Poorly", "Precisely", "Promptly",
- "Quickly", "Quietly", "Randomly", "Rapidly", "Rarely", "Recklessly", "Regularly", "Remorsefully", "Responsibly",
- "Rudely", "Ruthlessly", "Sadly", "Scornfully", "Seamlessly", "Seldom", "Selfishly", "Seriously", "Shakily",
- "Sharply", "Sideways", "Silently", "Sleepily", "Slightly", "Slowly", "Slyly", "Smoothly", "Softly", "Solemnly", "Steadily", "Sternly", "Strangely", "Strongly", "Stunningly", "Surely", "Tenderly", "Thoughtfully",
- "Tightly", "Uneasily", "Vanishingly", "Violently", "Warmly", "Weakly", "Wearily", "Weekly", "Weirdly", "Well",
- "Well", "Wickedly", "Wildly", "Wisely", "Wonderfully", "Yearly"
-];
-var adjectives = [
- "Abominable", "Accurate", "Adorable", "All", "Alleged", "Ancient", "Angry", "Angry", "Anxious", "Appalling",
- "Apparent", "Astonishing", "Attractive", "Awesome", "Baby", "Bad", "Beautiful", "Benign", "Big", "Bitter",
- "Blind", "Blue", "Bold", "Brave", "Bright", "Brisk", "Calm", "Camouflaged", "Casual", "Cautious",
- "Choppy", "Chosen", "Clever", "Cold", "Cool", "Crawly", "Crazy", "Creepy", "Cruel", "Curious", "Cynical",
- "Dangerous", "Dark", "Delicate", "Desperate", "Difficult", "Discreet", "Disguised", "Dizzy",
- "Dumb", "Eager", "Easy", "Edgy", "Electric", "Elegant", "Emancipated", "Enormous", "Euphoric", "Evil",
- "FAIL", "Fast", "Ferocious", "Fierce", "Fine", "Flawed", "Flying", "Foolish", "Foxy",
- "Freezing", "Funny", "Furious", "Gentle", "Glorious", "Golden", "Good", "Green", "Green", "Guilty",
- "Hairy", "Happy", "Hard", "Hasty", "Hazy", "Heroic", "Hostile", "Hot", "Humble", "Humongous",
- "Humorous", "Hysterical", "Idealistic", "Ignorant", "Immense", "Impartial", "Impolite", "Indifferent",
- "Infuriated", "Insightful", "Intense", "Interesting", "Intimidated", "Intriguing", "Jealous", "Jolly", "Jovial",
- "Jumpy", "Kind", "Laughing", "Lazy", "Liquid", "Lonely", "Longing", "Loud", "Loving", "Loyal", "Macabre", "Mad",
- "Magical", "Magnificent", "Malevolent", "Medieval", "Memorable", "Mere", "Merry", "Mighty",
- "Mischievous", "Miserable", "Modified", "Moody", "Most", "Mysterious", "Mystical", "Needy",
- "Nervous", "Nice", "Objective", "Obnoxious", "Obsessive", "Obvious", "Opinionated", "Orange",
- "Painful", "Passionate", "Perfect", "Pink", "Playful", "Poisonous", "Polite", "Poor", "Popular", "Powerful",
- "Precise", "Preserved", "Pretty", "Purple", "Quick", "Quiet", "Random", "Rapid", "Rare", "Real",
- "Reassuring", "Reckless", "Red", "Regular", "Remorseful", "Responsible", "Rich", "Rude", "Ruthless",
- "Sad", "Scared", "Scary", "Scornful", "Screaming", "Selfish", "Serious", "Shady", "Shaky", "Sharp",
- "Shiny", "Shy", "Simple", "Sleepy", "Slow", "Sly", "Small", "Smart", "Smelly", "Smiling", "Smooth",
- "Smug", "Sober", "Soft", "Solemn", "Square", "Square", "Steady", "Strange", "Strong",
- "Stunning", "Subjective", "Successful", "Surly", "Sweet", "Tactful", "Tense",
- "Thoughtful", "Tight", "Tiny", "Tolerant", "Uneasy", "Unique", "Unseen", "Warm", "Weak",
- "Weird", "WellCooked", "Wild", "Wise", "Witty", "Wonderful", "Worried", "Yellow", "Young",
- "Zealous"
- ];
-//var pronouns = [
-//];
-//var conjunctions = [
-//"And", "Or", "For", "Above", "Before", "Against", "Between"
-//];
-
-/*
- * Maps a string (category name) to the array of words from that category.
- */
-var CATEGORIES =
-{
- //"_NOUN_": nouns,
- "_PLURALNOUN_": pluralNouns,
- //"_PLACE_": places,
- "_VERB_": verbs,
- "_ADVERB_": adverbs,
- "_ADJECTIVE_": adjectives
- //"_PRONOUN_": pronouns,
- //"_CONJUNCTION_": conjunctions,
-};
-
-var PATTERNS = [
- "_ADJECTIVE__PLURALNOUN__VERB__ADVERB_"
-
- // BeautifulFungiOrSpaghetti
- //"_ADJECTIVE__PLURALNOUN__CONJUNCTION__PLURALNOUN_",
-
- // AmazinglyScaryToy
- //"_ADVERB__ADJECTIVE__NOUN_",
-
- // NeitherTrashNorRifle
- //"Neither_NOUN_Nor_NOUN_",
- //"Either_NOUN_Or_NOUN_",
-
- // EitherCopulateOrInvestigate
- //"Either_VERB_Or_VERB_",
- //"Neither_VERB_Nor_VERB_",
-
- //"The_ADJECTIVE__ADJECTIVE__NOUN_",
- //"The_ADVERB__ADJECTIVE__NOUN_",
- //"The_ADVERB__ADJECTIVE__NOUN_s",
- //"The_ADVERB__ADJECTIVE__PLURALNOUN__VERB_",
-
- // WolvesComputeBadly
- //"_PLURALNOUN__VERB__ADVERB_",
-
- // UniteFacilitateAndMerge
- //"_VERB__VERB_And_VERB_",
-
- //NastyWitchesAtThePub
- //"_ADJECTIVE__PLURALNOUN_AtThe_PLACE_",
-];
-
-
-/*
- * Returns a random element from the array 'arr'
- */
-function randomElement(arr)
-{
- return arr[Math.floor(Math.random() * arr.length)];
-}
-
-/*
- * Returns true if the string 's' contains one of the
- * template strings.
- */
-function hasTemplate(s)
-{
- for (var template in CATEGORIES){
- if (s.indexOf(template) >= 0){
- return true;
- }
- }
-}
-
-/**
- * Generates new room name.
- */
-var RoomNameGenerator = {
- generateRoomWithoutSeparator: function()
- {
- // Note that if more than one pattern is available, the choice of 'name' won't be random (names from patterns
- // with fewer options will have higher probability of being chosen that names from patterns with more options).
- var name = randomElement(PATTERNS);
- var word;
- while (hasTemplate(name)){
- for (var template in CATEGORIES){
- word = randomElement(CATEGORIES[template]);
- name = name.replace(template, word);
- }
- }
-
- return name;
- }
-}
-
-module.exports = RoomNameGenerator;
-
-},{}],34:[function(require,module,exports){
-var animateTimeout, updateTimeout;
-
-var RoomNameGenerator = require("./RoomnameGenerator");
-
-function enter_room()
-{
- var val = $("#enter_room_field").val();
- if(!val) {
- val = $("#enter_room_field").attr("room_name");
- }
- if (val) {
- window.location.pathname = "/" + val;
- }
-}
-
-function animate(word) {
- var currentVal = $("#enter_room_field").attr("placeholder");
- $("#enter_room_field").attr("placeholder", currentVal + word.substr(0, 1));
- animateTimeout = setTimeout(function() {
- animate(word.substring(1, word.length))
- }, 70);
-}
-
-function update_roomname()
-{
- var word = RoomNameGenerator.generateRoomWithoutSeparator();
- $("#enter_room_field").attr("room_name", word);
- $("#enter_room_field").attr("placeholder", "");
- clearTimeout(animateTimeout);
- animate(word);
- updateTimeout = setTimeout(update_roomname, 10000);
-}
-
-
-function setupWelcomePage()
-{
- $("#videoconference_page").hide();
- $("#domain_name").text(
- window.location.protocol + "//" + window.location.host + "/");
- if (interfaceConfig.SHOW_JITSI_WATERMARK) {
- var leftWatermarkDiv
- = $("#welcome_page_header div[class='watermark leftwatermark']");
- if(leftWatermarkDiv && leftWatermarkDiv.length > 0)
- {
- leftWatermarkDiv.css({display: 'block'});
- leftWatermarkDiv.parent().get(0).href
- = interfaceConfig.JITSI_WATERMARK_LINK;
- }
-
- }
-
- if (interfaceConfig.SHOW_BRAND_WATERMARK) {
- var rightWatermarkDiv
- = $("#welcome_page_header div[class='watermark rightwatermark']");
- if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
- rightWatermarkDiv.css({display: 'block'});
- rightWatermarkDiv.parent().get(0).href
- = interfaceConfig.BRAND_WATERMARK_LINK;
- rightWatermarkDiv.get(0).style.backgroundImage
- = "url(images/rightwatermark.png)";
- }
- }
-
- if (interfaceConfig.SHOW_POWERED_BY) {
- $("#welcome_page_header>a[class='poweredby']")
- .css({display: 'block'});
- }
-
- $("#enter_room_button").click(function()
- {
- enter_room();
- });
-
- $("#enter_room_field").keydown(function (event) {
- if (event.keyCode === 13 /* enter */) {
- enter_room();
- }
- });
-
- if (!(interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE === false)){
- var updateTimeout;
- var animateTimeout;
- $("#reload_roomname").click(function () {
- clearTimeout(updateTimeout);
- clearTimeout(animateTimeout);
- update_roomname();
- });
- $("#reload_roomname").show();
-
-
- update_roomname();
- }
-
- $("#disable_welcome").click(function () {
- window.localStorage.welcomePageDisabled
- = $("#disable_welcome").is(":checked");
- });
-
-}
+},{"../../../service/RTC/MediaStreamTypes":88,"../../../service/UI/UIEvents":93,"../audio_levels/AudioLevels":10,"../avatar/Avatar":14,"../etherpad/Etherpad":15,"../prezi/Prezi":16,"../side_pannels/chat/Chat":19,"../side_pannels/contactlist/ContactList":23,"../util/NicknameHandler":30,"../util/UIUtil":31,"./ConnectionIndicator":32}],34:[function(require,module,exports){
+//var nouns = [
+//];
+var pluralNouns = [
+ "Aliens", "Animals", "Antelopes", "Ants", "Apes", "Apples", "Baboons", "Bacteria", "Badgers", "Bananas", "Bats",
+ "Bears", "Birds", "Bonobos", "Brides", "Bugs", "Bulls", "Butterflies", "Cheetahs",
+ "Cherries", "Chicken", "Children", "Chimps", "Clowns", "Cows", "Creatures", "Dinosaurs", "Dogs", "Dolphins",
+ "Donkeys", "Dragons", "Ducks", "Dwarfs", "Eagles", "Elephants", "Elves", "FAIL", "Fathers",
+ "Fish", "Flowers", "Frogs", "Fruit", "Fungi", "Galaxies", "Geese", "Goats",
+ "Gorillas", "Hedgehogs", "Hippos", "Horses", "Hunters", "Insects", "Kids", "Knights",
+ "Lemons", "Lemurs", "Leopards", "LifeForms", "Lions", "Lizards", "Mice", "Monkeys", "Monsters",
+ "Mushrooms", "Octopodes", "Oranges", "Orangutans", "Organisms", "Pants", "Parrots", "Penguins",
+ "People", "Pigeons", "Pigs", "Pineapples", "Plants", "Potatoes", "Priests", "Rats", "Reptiles", "Reptilians",
+ "Rhinos", "Seagulls", "Sheep", "Siblings", "Snakes", "Spaghetti", "Spiders", "Squid", "Squirrels",
+ "Stars", "Students", "Teachers", "Tigers", "Tomatoes", "Trees", "Vampires", "Vegetables", "Viruses", "Vulcans",
+ "Warewolves", "Weasels", "Whales", "Witches", "Wizards", "Wolves", "Workers", "Worms", "Zebras"
+];
+//var places = [
+//"Pub", "University", "Airport", "Library", "Mall", "Theater", "Stadium", "Office", "Show", "Gallows", "Beach",
+// "Cemetery", "Hospital", "Reception", "Restaurant", "Bar", "Church", "House", "School", "Square", "Village",
+// "Cinema", "Movies", "Party", "Restroom", "End", "Jail", "PostOffice", "Station", "Circus", "Gates", "Entrance",
+// "Bridge"
+//];
+var verbs = [
+ "Abandon", "Adapt", "Advertise", "Answer", "Anticipate", "Appreciate",
+ "Approach", "Argue", "Ask", "Bite", "Blossom", "Blush", "Breathe", "Breed", "Bribe", "Burn", "Calculate",
+ "Clean", "Code", "Communicate", "Compute", "Confess", "Confiscate", "Conjugate", "Conjure", "Consume",
+ "Contemplate", "Crawl", "Dance", "Delegate", "Devour", "Develop", "Differ", "Discuss",
+ "Dissolve", "Drink", "Eat", "Elaborate", "Emancipate", "Estimate", "Expire", "Extinguish",
+ "Extract", "FAIL", "Facilitate", "Fall", "Feed", "Finish", "Floss", "Fly", "Follow", "Fragment", "Freeze",
+ "Gather", "Glow", "Grow", "Hex", "Hide", "Hug", "Hurry", "Improve", "Intersect", "Investigate", "Jinx",
+ "Joke", "Jubilate", "Kiss", "Laugh", "Manage", "Meet", "Merge", "Move", "Object", "Observe", "Offer",
+ "Paint", "Participate", "Party", "Perform", "Plan", "Pursue", "Pierce", "Play", "Postpone", "Pray", "Proclaim",
+ "Question", "Read", "Reckon", "Rejoice", "Represent", "Resize", "Rhyme", "Scream", "Search", "Select", "Share", "Shoot",
+ "Shout", "Signal", "Sing", "Skate", "Sleep", "Smile", "Smoke", "Solve", "Spell", "Steer", "Stink",
+ "Substitute", "Swim", "Taste", "Teach", "Terminate", "Think", "Type", "Unite", "Vanish", "Worship"
+];
+var adverbs = [
+ "Absently", "Accurately", "Accusingly", "Adorably", "AllTheTime", "Alone", "Always", "Amazingly", "Angrily",
+ "Anxiously", "Anywhere", "Appallingly", "Apparently", "Articulately", "Astonishingly", "Badly", "Barely",
+ "Beautifully", "Blindly", "Bravely", "Brightly", "Briskly", "Brutally", "Calmly", "Carefully", "Casually",
+ "Cautiously", "Cleverly", "Constantly", "Correctly", "Crazily", "Curiously", "Cynically", "Daily",
+ "Dangerously", "Deliberately", "Delicately", "Desperately", "Discreetly", "Eagerly", "Easily", "Euphoricly",
+ "Evenly", "Everywhere", "Exactly", "Expectantly", "Extensively", "FAIL", "Ferociously", "Fiercely", "Finely",
+ "Flatly", "Frequently", "Frighteningly", "Gently", "Gloriously", "Grimly", "Guiltily", "Happily",
+ "Hard", "Hastily", "Heroically", "High", "Highly", "Hourly", "Humbly", "Hysterically", "Immensely",
+ "Impartially", "Impolitely", "Indifferently", "Intensely", "Jealously", "Jovially", "Kindly", "Lazily",
+ "Lightly", "Loudly", "Lovingly", "Loyally", "Magnificently", "Malevolently", "Merrily", "Mightily", "Miserably",
+ "Mysteriously", "NOT", "Nervously", "Nicely", "Nowhere", "Objectively", "Obnoxiously", "Obsessively",
+ "Obviously", "Often", "Painfully", "Patiently", "Playfully", "Politely", "Poorly", "Precisely", "Promptly",
+ "Quickly", "Quietly", "Randomly", "Rapidly", "Rarely", "Recklessly", "Regularly", "Remorsefully", "Responsibly",
+ "Rudely", "Ruthlessly", "Sadly", "Scornfully", "Seamlessly", "Seldom", "Selfishly", "Seriously", "Shakily",
+ "Sharply", "Sideways", "Silently", "Sleepily", "Slightly", "Slowly", "Slyly", "Smoothly", "Softly", "Solemnly", "Steadily", "Sternly", "Strangely", "Strongly", "Stunningly", "Surely", "Tenderly", "Thoughtfully",
+ "Tightly", "Uneasily", "Vanishingly", "Violently", "Warmly", "Weakly", "Wearily", "Weekly", "Weirdly", "Well",
+ "Well", "Wickedly", "Wildly", "Wisely", "Wonderfully", "Yearly"
+];
+var adjectives = [
+ "Abominable", "Accurate", "Adorable", "All", "Alleged", "Ancient", "Angry", "Angry", "Anxious", "Appalling",
+ "Apparent", "Astonishing", "Attractive", "Awesome", "Baby", "Bad", "Beautiful", "Benign", "Big", "Bitter",
+ "Blind", "Blue", "Bold", "Brave", "Bright", "Brisk", "Calm", "Camouflaged", "Casual", "Cautious",
+ "Choppy", "Chosen", "Clever", "Cold", "Cool", "Crawly", "Crazy", "Creepy", "Cruel", "Curious", "Cynical",
+ "Dangerous", "Dark", "Delicate", "Desperate", "Difficult", "Discreet", "Disguised", "Dizzy",
+ "Dumb", "Eager", "Easy", "Edgy", "Electric", "Elegant", "Emancipated", "Enormous", "Euphoric", "Evil",
+ "FAIL", "Fast", "Ferocious", "Fierce", "Fine", "Flawed", "Flying", "Foolish", "Foxy",
+ "Freezing", "Funny", "Furious", "Gentle", "Glorious", "Golden", "Good", "Green", "Green", "Guilty",
+ "Hairy", "Happy", "Hard", "Hasty", "Hazy", "Heroic", "Hostile", "Hot", "Humble", "Humongous",
+ "Humorous", "Hysterical", "Idealistic", "Ignorant", "Immense", "Impartial", "Impolite", "Indifferent",
+ "Infuriated", "Insightful", "Intense", "Interesting", "Intimidated", "Intriguing", "Jealous", "Jolly", "Jovial",
+ "Jumpy", "Kind", "Laughing", "Lazy", "Liquid", "Lonely", "Longing", "Loud", "Loving", "Loyal", "Macabre", "Mad",
+ "Magical", "Magnificent", "Malevolent", "Medieval", "Memorable", "Mere", "Merry", "Mighty",
+ "Mischievous", "Miserable", "Modified", "Moody", "Most", "Mysterious", "Mystical", "Needy",
+ "Nervous", "Nice", "Objective", "Obnoxious", "Obsessive", "Obvious", "Opinionated", "Orange",
+ "Painful", "Passionate", "Perfect", "Pink", "Playful", "Poisonous", "Polite", "Poor", "Popular", "Powerful",
+ "Precise", "Preserved", "Pretty", "Purple", "Quick", "Quiet", "Random", "Rapid", "Rare", "Real",
+ "Reassuring", "Reckless", "Red", "Regular", "Remorseful", "Responsible", "Rich", "Rude", "Ruthless",
+ "Sad", "Scared", "Scary", "Scornful", "Screaming", "Selfish", "Serious", "Shady", "Shaky", "Sharp",
+ "Shiny", "Shy", "Simple", "Sleepy", "Slow", "Sly", "Small", "Smart", "Smelly", "Smiling", "Smooth",
+ "Smug", "Sober", "Soft", "Solemn", "Square", "Square", "Steady", "Strange", "Strong",
+ "Stunning", "Subjective", "Successful", "Surly", "Sweet", "Tactful", "Tense",
+ "Thoughtful", "Tight", "Tiny", "Tolerant", "Uneasy", "Unique", "Unseen", "Warm", "Weak",
+ "Weird", "WellCooked", "Wild", "Wise", "Witty", "Wonderful", "Worried", "Yellow", "Young",
+ "Zealous"
+ ];
+//var pronouns = [
+//];
+//var conjunctions = [
+//"And", "Or", "For", "Above", "Before", "Against", "Between"
+//];
+
+/*
+ * Maps a string (category name) to the array of words from that category.
+ */
+var CATEGORIES =
+{
+ //"_NOUN_": nouns,
+ "_PLURALNOUN_": pluralNouns,
+ //"_PLACE_": places,
+ "_VERB_": verbs,
+ "_ADVERB_": adverbs,
+ "_ADJECTIVE_": adjectives
+ //"_PRONOUN_": pronouns,
+ //"_CONJUNCTION_": conjunctions,
+};
+
+var PATTERNS = [
+ "_ADJECTIVE__PLURALNOUN__VERB__ADVERB_"
+
+ // BeautifulFungiOrSpaghetti
+ //"_ADJECTIVE__PLURALNOUN__CONJUNCTION__PLURALNOUN_",
+
+ // AmazinglyScaryToy
+ //"_ADVERB__ADJECTIVE__NOUN_",
+
+ // NeitherTrashNorRifle
+ //"Neither_NOUN_Nor_NOUN_",
+ //"Either_NOUN_Or_NOUN_",
+
+ // EitherCopulateOrInvestigate
+ //"Either_VERB_Or_VERB_",
+ //"Neither_VERB_Nor_VERB_",
+
+ //"The_ADJECTIVE__ADJECTIVE__NOUN_",
+ //"The_ADVERB__ADJECTIVE__NOUN_",
+ //"The_ADVERB__ADJECTIVE__NOUN_s",
+ //"The_ADVERB__ADJECTIVE__PLURALNOUN__VERB_",
+
+ // WolvesComputeBadly
+ //"_PLURALNOUN__VERB__ADVERB_",
+
+ // UniteFacilitateAndMerge
+ //"_VERB__VERB_And_VERB_",
+
+ //NastyWitchesAtThePub
+ //"_ADJECTIVE__PLURALNOUN_AtThe_PLACE_",
+];
+
+
+/*
+ * Returns a random element from the array 'arr'
+ */
+function randomElement(arr)
+{
+ return arr[Math.floor(Math.random() * arr.length)];
+}
+
+/*
+ * Returns true if the string 's' contains one of the
+ * template strings.
+ */
+function hasTemplate(s)
+{
+ for (var template in CATEGORIES){
+ if (s.indexOf(template) >= 0){
+ return true;
+ }
+ }
+}
+
+/**
+ * Generates new room name.
+ */
+var RoomNameGenerator = {
+ generateRoomWithoutSeparator: function()
+ {
+ // Note that if more than one pattern is available, the choice of 'name' won't be random (names from patterns
+ // with fewer options will have higher probability of being chosen that names from patterns with more options).
+ var name = randomElement(PATTERNS);
+ var word;
+ while (hasTemplate(name)){
+ for (var template in CATEGORIES){
+ word = randomElement(CATEGORIES[template]);
+ name = name.replace(template, word);
+ }
+ }
+
+ return name;
+ }
+}
+
+module.exports = RoomNameGenerator;
+},{}],35:[function(require,module,exports){
+var animateTimeout, updateTimeout;
+
+var RoomNameGenerator = require("./RoomnameGenerator");
+
+function enter_room()
+{
+ var val = $("#enter_room_field").val();
+ if(!val) {
+ val = $("#enter_room_field").attr("room_name");
+ }
+ if (val) {
+ window.location.pathname = "/" + val;
+ }
+}
+
+function animate(word) {
+ var currentVal = $("#enter_room_field").attr("placeholder");
+ $("#enter_room_field").attr("placeholder", currentVal + word.substr(0, 1));
+ animateTimeout = setTimeout(function() {
+ animate(word.substring(1, word.length))
+ }, 70);
+}
+
+function update_roomname()
+{
+ var word = RoomNameGenerator.generateRoomWithoutSeparator();
+ $("#enter_room_field").attr("room_name", word);
+ $("#enter_room_field").attr("placeholder", "");
+ clearTimeout(animateTimeout);
+ animate(word);
+ updateTimeout = setTimeout(update_roomname, 10000);
+}
+
+
+function setupWelcomePage()
+{
+ $("#videoconference_page").hide();
+ $("#domain_name").text(
+ window.location.protocol + "//" + window.location.host + "/");
+ if (interfaceConfig.SHOW_JITSI_WATERMARK) {
+ var leftWatermarkDiv
+ = $("#welcome_page_header div[class='watermark leftwatermark']");
+ if(leftWatermarkDiv && leftWatermarkDiv.length > 0)
+ {
+ leftWatermarkDiv.css({display: 'block'});
+ leftWatermarkDiv.parent().get(0).href
+ = interfaceConfig.JITSI_WATERMARK_LINK;
+ }
+
+ }
+
+ if (interfaceConfig.SHOW_BRAND_WATERMARK) {
+ var rightWatermarkDiv
+ = $("#welcome_page_header div[class='watermark rightwatermark']");
+ if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
+ rightWatermarkDiv.css({display: 'block'});
+ rightWatermarkDiv.parent().get(0).href
+ = interfaceConfig.BRAND_WATERMARK_LINK;
+ rightWatermarkDiv.get(0).style.backgroundImage
+ = "url(images/rightwatermark.png)";
+ }
+ }
+
+ if (interfaceConfig.SHOW_POWERED_BY) {
+ $("#welcome_page_header>a[class='poweredby']")
+ .css({display: 'block'});
+ }
+
+ $("#enter_room_button").click(function()
+ {
+ enter_room();
+ });
+
+ $("#enter_room_field").keydown(function (event) {
+ if (event.keyCode === 13 /* enter */) {
+ enter_room();
+ }
+ });
+
+ if (!(interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE === false)){
+ var updateTimeout;
+ var animateTimeout;
+ $("#reload_roomname").click(function () {
+ clearTimeout(updateTimeout);
+ clearTimeout(animateTimeout);
+ update_roomname();
+ });
+ $("#reload_roomname").show();
+
+
+ update_roomname();
+ }
+
+ $("#disable_welcome").click(function () {
+ window.localStorage.welcomePageDisabled
+ = $("#disable_welcome").is(":checked");
+ });
+
+}
+
module.exports = setupWelcomePage;
-},{"./RoomnameGenerator":33}],35:[function(require,module,exports){
-var EventEmitter = require("events");
-var eventEmitter = new EventEmitter();
-var CQEvents = require("../../service/connectionquality/CQEvents");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-
-/**
- * local stats
- * @type {{}}
- */
-var stats = {};
-
-/**
- * remote stats
- * @type {{}}
- */
-var remoteStats = {};
-
-/**
- * Interval for sending statistics to other participants
- * @type {null}
- */
-var sendIntervalId = null;
-
-
-/**
- * Start statistics sending.
- */
-function startSendingStats() {
- sendStats();
- sendIntervalId = setInterval(sendStats, 10000);
-}
-
-/**
- * Sends statistics to other participants
- */
-function sendStats() {
- APP.xmpp.addToPresence("connectionQuality", convertToMUCStats(stats));
-}
-
-/**
- * Converts statistics to format for sending through XMPP
- * @param stats the statistics
- * @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}}
- */
-function convertToMUCStats(stats) {
- return {
- "bitrate_download": stats.bitrate.download,
- "bitrate_upload": stats.bitrate.upload,
- "packetLoss_total": stats.packetLoss.total,
- "packetLoss_download": stats.packetLoss.download,
- "packetLoss_upload": stats.packetLoss.upload
- };
-}
-
-/**
- * Converts statitistics to format used by VideoLayout
- * @param stats
- * @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
- */
-function parseMUCStats(stats) {
- return {
- bitrate: {
- download: stats.bitrate_download,
- upload: stats.bitrate_upload
- },
- packetLoss: {
- total: stats.packetLoss_total,
- download: stats.packetLoss_download,
- upload: stats.packetLoss_upload
- }
- };
-}
-
-
-var ConnectionQuality = {
- init: function () {
- APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats);
- APP.statistics.addConnectionStatsListener(this.updateLocalStats);
- APP.statistics.addRemoteStatsStopListener(this.stopSendingStats);
-
- },
-
- /**
- * Updates the local statistics
- * @param data new statistics
- */
- updateLocalStats: function (data) {
- stats = data;
- eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
- if (sendIntervalId == null) {
- startSendingStats();
- }
- },
-
- /**
- * Updates remote statistics
- * @param jid the jid associated with the statistics
- * @param data the statistics
- */
- updateRemoteStats: function (jid, data) {
- if (data == null || data.packetLoss_total == null) {
- eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null);
- return;
- }
- remoteStats[jid] = parseMUCStats(data);
-
- eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED,
- jid, 100 - data.packetLoss_total, remoteStats[jid]);
- },
-
- /**
- * Stops statistics sending.
- */
- stopSendingStats: function () {
- clearInterval(sendIntervalId);
- sendIntervalId = null;
- //notify UI about stopping statistics gathering
- eventEmitter.emit(CQEvents.STOP);
- },
-
- /**
- * Returns the local statistics.
- */
- getStats: function () {
- return stats;
- },
-
- addListener: function (type, listener) {
- eventEmitter.on(type, listener);
- }
-
-};
-
+},{"./RoomnameGenerator":34}],36:[function(require,module,exports){
+var EventEmitter = require("events");
+var eventEmitter = new EventEmitter();
+var CQEvents = require("../../service/connectionquality/CQEvents");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+
+/**
+ * local stats
+ * @type {{}}
+ */
+var stats = {};
+
+/**
+ * remote stats
+ * @type {{}}
+ */
+var remoteStats = {};
+
+/**
+ * Interval for sending statistics to other participants
+ * @type {null}
+ */
+var sendIntervalId = null;
+
+
+/**
+ * Start statistics sending.
+ */
+function startSendingStats() {
+ sendStats();
+ sendIntervalId = setInterval(sendStats, 10000);
+}
+
+/**
+ * Sends statistics to other participants
+ */
+function sendStats() {
+ APP.xmpp.addToPresence("connectionQuality", convertToMUCStats(stats));
+}
+
+/**
+ * Converts statistics to format for sending through XMPP
+ * @param stats the statistics
+ * @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}}
+ */
+function convertToMUCStats(stats) {
+ return {
+ "bitrate_download": stats.bitrate.download,
+ "bitrate_upload": stats.bitrate.upload,
+ "packetLoss_total": stats.packetLoss.total,
+ "packetLoss_download": stats.packetLoss.download,
+ "packetLoss_upload": stats.packetLoss.upload
+ };
+}
+
+/**
+ * Converts statitistics to format used by VideoLayout
+ * @param stats
+ * @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
+ */
+function parseMUCStats(stats) {
+ return {
+ bitrate: {
+ download: stats.bitrate_download,
+ upload: stats.bitrate_upload
+ },
+ packetLoss: {
+ total: stats.packetLoss_total,
+ download: stats.packetLoss_download,
+ upload: stats.packetLoss_upload
+ }
+ };
+}
+
+
+var ConnectionQuality = {
+ init: function () {
+ APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats);
+ APP.statistics.addConnectionStatsListener(this.updateLocalStats);
+ APP.statistics.addRemoteStatsStopListener(this.stopSendingStats);
+
+ },
+
+ /**
+ * Updates the local statistics
+ * @param data new statistics
+ */
+ updateLocalStats: function (data) {
+ stats = data;
+ eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
+ if (sendIntervalId == null) {
+ startSendingStats();
+ }
+ },
+
+ /**
+ * Updates remote statistics
+ * @param jid the jid associated with the statistics
+ * @param data the statistics
+ */
+ updateRemoteStats: function (jid, data) {
+ if (data == null || data.packetLoss_total == null) {
+ eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null);
+ return;
+ }
+ remoteStats[jid] = parseMUCStats(data);
+
+ eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED,
+ jid, 100 - data.packetLoss_total, remoteStats[jid]);
+ },
+
+ /**
+ * Stops statistics sending.
+ */
+ stopSendingStats: function () {
+ clearInterval(sendIntervalId);
+ sendIntervalId = null;
+ //notify UI about stopping statistics gathering
+ eventEmitter.emit(CQEvents.STOP);
+ },
+
+ /**
+ * Returns the local statistics.
+ */
+ getStats: function () {
+ return stats;
+ },
+
+ addListener: function (type, listener) {
+ eventEmitter.on(type, listener);
+ }
+
+};
+
module.exports = ConnectionQuality;
-},{"../../service/connectionquality/CQEvents":94,"../../service/xmpp/XMPPEvents":97,"events":98}],36:[function(require,module,exports){
-/* global $, alert, changeLocalVideo, chrome, config, getConferenceHandler, getUserMediaWithConstraints */
-/**
- * Indicates that desktop stream is currently in use(for toggle purpose).
- * @type {boolean}
- */
-var isUsingScreenStream = false;
-/**
- * Indicates that switch stream operation is in progress and prevent from triggering new events.
- * @type {boolean}
- */
-var switchInProgress = false;
+},{"../../service/connectionquality/CQEvents":95,"../../service/xmpp/XMPPEvents":98,"events":1}],37:[function(require,module,exports){
+/* global $, alert, APP, changeLocalVideo, chrome, config, getConferenceHandler,
+ getUserMediaWithConstraints */
+/**
+ * Indicates that desktop stream is currently in use(for toggle purpose).
+ * @type {boolean}
+ */
+var isUsingScreenStream = false;
+/**
+ * Indicates that switch stream operation is in progress and prevent from
+ * triggering new events.
+ * @type {boolean}
+ */
+var switchInProgress = false;
+
+/**
+ * Method used to get screen sharing stream.
+ *
+ * @type {function (stream_callback, failure_callback}
+ */
+var obtainDesktopStream = null;
+
+/**
+ * Indicates whether desktop sharing extension is installed.
+ * @type {boolean}
+ */
+var extInstalled = false;
+
+/**
+ * Indicates whether update of desktop sharing extension is required.
+ * @type {boolean}
+ */
+var extUpdateRequired = false;
+
+/**
+ * Flag used to cache desktop sharing enabled state. Do not use directly as
+ * it can be null .
+ *
+ * @type {null|boolean}
+ */
+var _desktopSharingEnabled = null;
+
+var EventEmitter = require("events");
+
+var eventEmitter = new EventEmitter();
+
+var DesktopSharingEventTypes
+ = require("../../service/desktopsharing/DesktopSharingEventTypes");
+
+/**
+ * Method obtains desktop stream from WebRTC 'screen' source.
+ * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
+ */
+function obtainWebRTCScreen(streamCallback, failCallback) {
+ APP.RTC.getUserMediaWithConstraints(
+ ['screen'],
+ streamCallback,
+ failCallback
+ );
+}
+
+/**
+ * Constructs inline install URL for Chrome desktop streaming extension.
+ * The 'chromeExtensionId' must be defined in config.js.
+ * @returns {string}
+ */
+function getWebStoreInstallUrl()
+{
+ return "https://chrome.google.com/webstore/detail/" +
+ config.chromeExtensionId;
+}
+
+/**
+ * Checks whether extension update is required.
+ * @param minVersion minimal required version
+ * @param extVersion current extension version
+ * @returns {boolean}
+ */
+function isUpdateRequired(minVersion, extVersion)
+{
+ try
+ {
+ var s1 = minVersion.split('.');
+ var s2 = extVersion.split('.');
+
+ var len = Math.max(s1.length, s2.length);
+ for (var i = 0; i < len; i++)
+ {
+ var n1 = 0,
+ n2 = 0;
+
+ if (i < s1.length)
+ n1 = parseInt(s1[i]);
+ if (i < s2.length)
+ n2 = parseInt(s2[i]);
+
+ if (isNaN(n1) || isNaN(n2))
+ {
+ return true;
+ }
+ else if (n1 !== n2)
+ {
+ return n1 > n2;
+ }
+ }
+
+ // will happen if boths version has identical numbers in
+ // their components (even if one of them is longer, has more components)
+ return false;
+ }
+ catch (e)
+ {
+ console.error("Failed to parse extension version", e);
+ APP.UI.messageHandler.showError("dialog.error",
+ "dialog.detectext");
+ return true;
+ }
+}
+
+function checkExtInstalled(callback) {
+ if (!chrome.runtime) {
+ // No API, so no extension for sure
+ callback(false, false);
+ return;
+ }
+ chrome.runtime.sendMessage(
+ config.chromeExtensionId,
+ { getVersion: true },
+ function (response) {
+ if (!response || !response.version) {
+ // Communication failure - assume that no endpoint exists
+ console.warn(
+ "Extension not installed?: ", chrome.runtime.lastError);
+ callback(false, false);
+ return;
+ }
+ // Check installed extension version
+ var extVersion = response.version;
+ console.log('Extension version is: ' + extVersion);
+ var updateRequired
+ = isUpdateRequired(config.minChromeExtVersion, extVersion);
+ callback(!updateRequired, updateRequired);
+ }
+ );
+}
+
+function doGetStreamFromExtension(streamCallback, failCallback) {
+ // Sends 'getStream' msg to the extension.
+ // Extension id must be defined in the config.
+ chrome.runtime.sendMessage(
+ config.chromeExtensionId,
+ { getStream: true, sources: config.desktopSharingSources },
+ function (response) {
+ if (!response) {
+ failCallback(chrome.runtime.lastError);
+ return;
+ }
+ console.log("Response from extension: " + response);
+ if (response.streamId) {
+ APP.RTC.getUserMediaWithConstraints(
+ ['desktop'],
+ function (stream) {
+ streamCallback(stream);
+ },
+ failCallback,
+ null, null, null,
+ response.streamId);
+ } else {
+ failCallback("Extension failed to get the stream");
+ }
+ }
+ );
+}
+/**
+ * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop'
+ * stream for returned stream token.
+ */
+function obtainScreenFromExtension(streamCallback, failCallback) {
+ if (extInstalled) {
+ doGetStreamFromExtension(streamCallback, failCallback);
+ } else {
+ if (extUpdateRequired) {
+ alert(
+ 'Jitsi Desktop Streamer requires update. ' +
+ 'Changes will take effect after next Chrome restart.');
+ }
+
+ chrome.webstore.install(
+ getWebStoreInstallUrl(),
+ function (arg) {
+ console.log("Extension installed successfully", arg);
+ // We need to reload the page in order to get the access to
+ // chrome.runtime
+ window.location.reload(false);
+ },
+ function (arg) {
+ console.log("Failed to install the extension", arg);
+ failCallback(arg);
+ APP.UI.messageHandler.showError("dialog.error",
+ "dialog.failtoinstall");
+ }
+ );
+ }
+}
+
+/**
+ * Call this method to toggle desktop sharing feature.
+ * @param method pass "ext" to use chrome extension for desktop capture(chrome
+ * extension required), pass "webrtc" to use WebRTC "screen" desktop
+ * source('chrome://flags/#enable-usermedia-screen-capture' must be
+ * enabled), pass any other string or nothing in order to disable this
+ * feature completely.
+ */
+function setDesktopSharing(method) {
+ // Check if we are running chrome
+ if (!navigator.webkitGetUserMedia) {
+ obtainDesktopStream = null;
+ console.info("Desktop sharing disabled");
+ } else if (method == "ext") {
+ obtainDesktopStream = obtainScreenFromExtension;
+ console.info("Using Chrome extension for desktop sharing");
+ } else if (method == "webrtc") {
+ obtainDesktopStream = obtainWebRTCScreen;
+ console.info("Using Chrome WebRTC for desktop sharing");
+ }
+
+ // Reset enabled cache
+ _desktopSharingEnabled = null;
+}
+
+/**
+ * Initializes with extension id set in
+ * config.js to support inline installs. Host site must be selected as main
+ * website of published extension.
+ */
+function initInlineInstalls()
+{
+ $("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl());
+}
+
+function getSwitchStreamFailed(error) {
+ console.error("Failed to obtain the stream to switch to", error);
+ switchInProgress = false;
+}
+
+function streamSwitchDone() {
+ switchInProgress = false;
+ eventEmitter.emit(
+ DesktopSharingEventTypes.SWITCHING_DONE,
+ isUsingScreenStream);
+}
+
+function newStreamCreated(stream)
+{
+ eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED,
+ stream, isUsingScreenStream, streamSwitchDone);
+}
+
+
+module.exports = {
+ isUsingScreenStream: function () {
+ return isUsingScreenStream;
+ },
+
+ /**
+ * @returns {boolean} true if desktop sharing feature is available
+ * and enabled.
+ */
+ isDesktopSharingEnabled: function () {
+ if (_desktopSharingEnabled === null) {
+ if (obtainDesktopStream === obtainScreenFromExtension) {
+ // Parse chrome version
+ var userAgent = navigator.userAgent.toLowerCase();
+ // We can assume that user agent is chrome, because it's
+ // enforced when 'ext' streaming method is set
+ var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
+ console.log("Chrome version" + userAgent, ver);
+ _desktopSharingEnabled = ver >= 34;
+ } else {
+ _desktopSharingEnabled =
+ obtainDesktopStream === obtainWebRTCScreen;
+ }
+ }
+ return _desktopSharingEnabled;
+ },
+
+ init: function () {
+ setDesktopSharing(config.desktopSharing);
+
+ // Initialize Chrome extension inline installs
+ if (config.chromeExtensionId) {
+
+ initInlineInstalls();
+
+ // Check if extension is installed
+ checkExtInstalled(function (installed, updateRequired) {
+ extInstalled = installed;
+ extUpdateRequired = updateRequired;
+ console.info(
+ "Chrome extension installed: " + extInstalled +
+ " updateRequired: " + extUpdateRequired);
+ });
+ }
+
+ eventEmitter.emit(DesktopSharingEventTypes.INIT);
+ },
+
+ addListener: function (listener, type)
+ {
+ eventEmitter.on(type, listener);
+ },
+
+ removeListener: function (listener, type) {
+ eventEmitter.removeListener(type, listener);
+ },
+
+ /*
+ * Toggles screen sharing.
+ */
+ toggleScreenSharing: function () {
+ if (switchInProgress || !obtainDesktopStream) {
+ console.warn("Switch in progress or no method defined");
+ return;
+ }
+ switchInProgress = true;
+
+ if (!isUsingScreenStream)
+ {
+ // Switch to desktop stream
+ obtainDesktopStream(
+ function (stream) {
+ // We now use screen stream
+ isUsingScreenStream = true;
+ // Hook 'ended' event to restore camera
+ // when screen stream stops
+ stream.addEventListener('ended',
+ function (e) {
+ if (!switchInProgress && isUsingScreenStream) {
+ APP.desktopsharing.toggleScreenSharing();
+ }
+ }
+ );
+ newStreamCreated(stream);
+ },
+ getSwitchStreamFailed);
+ } else {
+ // Disable screen stream
+ APP.RTC.getUserMediaWithConstraints(
+ ['video'],
+ function (stream) {
+ // We are now using camera stream
+ isUsingScreenStream = false;
+ newStreamCreated(stream);
+ },
+ getSwitchStreamFailed, config.resolution || '360'
+ );
+ }
+ }
+};
+
-/**
- * Method used to get screen sharing stream.
- *
- * @type {function (stream_callback, failure_callback}
- */
-var obtainDesktopStream = null;
-
-/**
- * Flag used to cache desktop sharing enabled state. Do not use directly as it can be null .
- * @type {null|boolean}
- */
-var _desktopSharingEnabled = null;
-
-var EventEmitter = require("events");
-
-var eventEmitter = new EventEmitter();
-
-var DesktopSharingEventTypes = require("../../service/desktopsharing/DesktopSharingEventTypes");
-
-/**
- * Method obtains desktop stream from WebRTC 'screen' source.
- * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
- */
-function obtainWebRTCScreen(streamCallback, failCallback) {
- APP.RTC.getUserMediaWithConstraints(
- ['screen'],
- streamCallback,
- failCallback
- );
-}
-
-/**
- * Constructs inline install URL for Chrome desktop streaming extension.
- * The 'chromeExtensionId' must be defined in config.js.
- * @returns {string}
- */
-function getWebStoreInstallUrl()
-{
- return "https://chrome.google.com/webstore/detail/" + config.chromeExtensionId;
-}
-
-/**
- * Checks whether extension update is required.
- * @param minVersion minimal required version
- * @param extVersion current extension version
- * @returns {boolean}
- */
-function isUpdateRequired(minVersion, extVersion)
-{
- try
- {
- var s1 = minVersion.split('.');
- var s2 = extVersion.split('.');
-
- var len = Math.max(s1.length, s2.length);
- for (var i = 0; i < len; i++)
- {
- var n1 = 0,
- n2 = 0;
-
- if (i < s1.length)
- n1 = parseInt(s1[i]);
- if (i < s2.length)
- n2 = parseInt(s2[i]);
-
- if (isNaN(n1) || isNaN(n2))
- {
- return true;
- }
- else if (n1 !== n2)
- {
- return n1 > n2;
- }
- }
-
- // will happen if boths version has identical numbers in
- // their components (even if one of them is longer, has more components)
- return false;
- }
- catch (e)
- {
- console.error("Failed to parse extension version", e);
- APP.UI.messageHandler.showError("dialog.error",
- "dialog.detectext");
- return true;
- }
-}
-
-
-function checkExtInstalled(isInstalledCallback) {
- if (!chrome.runtime) {
- // No API, so no extension for sure
- isInstalledCallback(false);
- return;
- }
- chrome.runtime.sendMessage(
- config.chromeExtensionId,
- { getVersion: true },
- function (response) {
- if (!response || !response.version) {
- // Communication failure - assume that no endpoint exists
- console.warn("Extension not installed?: " + chrome.runtime.lastError);
- isInstalledCallback(false);
- } else {
- // Check installed extension version
- var extVersion = response.version;
- console.log('Extension version is: ' + extVersion);
- var updateRequired = isUpdateRequired(config.minChromeExtVersion, extVersion);
- if (updateRequired) {
- alert(
- 'Jitsi Desktop Streamer requires update. ' +
- 'Changes will take effect after next Chrome restart.');
- }
- isInstalledCallback(!updateRequired);
- }
- }
- );
-}
-
-function doGetStreamFromExtension(streamCallback, failCallback) {
- // Sends 'getStream' msg to the extension. Extension id must be defined in the config.
- chrome.runtime.sendMessage(
- config.chromeExtensionId,
- { getStream: true, sources: config.desktopSharingSources },
- function (response) {
- if (!response) {
- failCallback(chrome.runtime.lastError);
- return;
- }
- console.log("Response from extension: " + response);
- if (response.streamId) {
- APP.RTC.getUserMediaWithConstraints(
- ['desktop'],
- function (stream) {
- streamCallback(stream);
- },
- failCallback,
- null, null, null,
- response.streamId);
- } else {
- failCallback("Extension failed to get the stream");
- }
- }
- );
-}
-/**
- * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' stream for returned stream token.
- */
-function obtainScreenFromExtension(streamCallback, failCallback) {
- checkExtInstalled(
- function (isInstalled) {
- if (isInstalled) {
- doGetStreamFromExtension(streamCallback, failCallback);
- } else {
- chrome.webstore.install(
- getWebStoreInstallUrl(),
- function (arg) {
- console.log("Extension installed successfully", arg);
- // We need to reload the page in order to get the access to chrome.runtime
- window.location.reload(false);
- },
- function (arg) {
- console.log("Failed to install the extension", arg);
- failCallback(arg);
- APP.UI.messageHandler.showError("dialog.error",
- "dialog.failtoinstall");
- }
- );
- }
- }
- );
-}
-
-/**
- * Call this method to toggle desktop sharing feature.
- * @param method pass "ext" to use chrome extension for desktop capture(chrome extension required),
- * pass "webrtc" to use WebRTC "screen" desktop source('chrome://flags/#enable-usermedia-screen-capture'
- * must be enabled), pass any other string or nothing in order to disable this feature completely.
- */
-function setDesktopSharing(method) {
- // Check if we are running chrome
- if (!navigator.webkitGetUserMedia) {
- obtainDesktopStream = null;
- console.info("Desktop sharing disabled");
- } else if (method == "ext") {
- obtainDesktopStream = obtainScreenFromExtension;
- console.info("Using Chrome extension for desktop sharing");
- } else if (method == "webrtc") {
- obtainDesktopStream = obtainWebRTCScreen;
- console.info("Using Chrome WebRTC for desktop sharing");
- }
-
- // Reset enabled cache
- _desktopSharingEnabled = null;
-}
-
-/**
- * Initializes with extension id set in config.js to support inline installs.
- * Host site must be selected as main website of published extension.
- */
-function initInlineInstalls()
-{
- $("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl());
-}
-
-function getSwitchStreamFailed(error) {
- console.error("Failed to obtain the stream to switch to", error);
- switchInProgress = false;
-}
-
-function streamSwitchDone() {
- switchInProgress = false;
- eventEmitter.emit(
- DesktopSharingEventTypes.SWITCHING_DONE,
- isUsingScreenStream);
-}
-
-function newStreamCreated(stream)
-{
- eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED,
- stream, isUsingScreenStream, streamSwitchDone);
-}
-
-
-module.exports = {
- isUsingScreenStream: function () {
- return isUsingScreenStream;
- },
-
- /**
- * @returns {boolean} true if desktop sharing feature is available and enabled.
- */
- isDesktopSharingEnabled: function () {
- if (_desktopSharingEnabled === null) {
- if (obtainDesktopStream === obtainScreenFromExtension) {
- // Parse chrome version
- var userAgent = navigator.userAgent.toLowerCase();
- // We can assume that user agent is chrome, because it's enforced when 'ext' streaming method is set
- var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
- console.log("Chrome version" + userAgent, ver);
- _desktopSharingEnabled = ver >= 34;
- } else {
- _desktopSharingEnabled = obtainDesktopStream === obtainWebRTCScreen;
- }
- }
- return _desktopSharingEnabled;
- },
-
- init: function () {
- setDesktopSharing(config.desktopSharing);
-
- // Initialize Chrome extension inline installs
- if (config.chromeExtensionId) {
- initInlineInstalls();
- }
-
- eventEmitter.emit(DesktopSharingEventTypes.INIT);
- },
-
- addListener: function(listener, type)
- {
- eventEmitter.on(type, listener);
- },
-
- removeListener: function (listener,type) {
- eventEmitter.removeListener(type, listener);
- },
-
- /*
- * Toggles screen sharing.
- */
- toggleScreenSharing: function () {
- if (switchInProgress || !obtainDesktopStream) {
- console.warn("Switch in progress or no method defined");
- return;
- }
- switchInProgress = true;
-
- if (!isUsingScreenStream)
- {
- // Switch to desktop stream
- obtainDesktopStream(
- function (stream) {
- // We now use screen stream
- isUsingScreenStream = true;
- // Hook 'ended' event to restore camera when screen stream stops
- stream.addEventListener('ended',
- function (e) {
- if (!switchInProgress && isUsingScreenStream) {
- toggleScreenSharing();
- }
- }
- );
- newStreamCreated(stream);
- },
- getSwitchStreamFailed);
- } else {
- // Disable screen stream
- APP.RTC.getUserMediaWithConstraints(
- ['video'],
- function (stream) {
- // We are now using camera stream
- isUsingScreenStream = false;
- newStreamCreated(stream);
- },
- getSwitchStreamFailed, config.resolution || '360'
- );
- }
- }
-};
-
-
-},{"../../service/desktopsharing/DesktopSharingEventTypes":95,"events":98}],37:[function(require,module,exports){
-//maps keycode to character, id of popover for given function and function
-var shortcuts = {
- 67: {
- character: "C",
- id: "toggleChatPopover",
- function: APP.UI.toggleChat
- },
- 70: {
- character: "F",
- id: "filmstripPopover",
- function: APP.UI.toggleFilmStrip
- },
- 77: {
- character: "M",
- id: "mutePopover",
- function: APP.UI.toggleAudio
- },
- 84: {
- character: "T",
- function: function() {
- if(!APP.RTC.localAudio.isMuted()) {
- APP.UI.toggleAudio();
- }
- }
- },
- 86: {
- character: "V",
- id: "toggleVideoPopover",
- function: APP.UI.toggleVideo
- }
-};
-
-
-var KeyboardShortcut = {
- init: function () {
- window.onkeyup = function(e) {
- var keycode = e.which;
- if(!($(":focus").is("input[type=text]") ||
- $(":focus").is("input[type=password]") ||
- $(":focus").is("textarea"))) {
- if (typeof shortcuts[keycode] === "object") {
- shortcuts[keycode].function();
- }
- else if (keycode >= "0".charCodeAt(0) &&
- keycode <= "9".charCodeAt(0)) {
- APP.UI.clickOnVideo(keycode - "0".charCodeAt(0) + 1);
- }
- //esc while the smileys are visible hides them
- } else if (keycode === 27 && $('#smileysContainer').is(':visible')) {
- APP.UI.toggleSmileys();
- }
- };
-
- window.onkeydown = function(e) {
- if(!($(":focus").is("input[type=text]") ||
- $(":focus").is("input[type=password]") ||
- $(":focus").is("textarea"))) {
- if(e.which === "T".charCodeAt(0)) {
- if(APP.RTC.localAudio.isMuted()) {
- APP.UI.toggleAudio();
- }
- }
- }
- };
- var self = this;
- $('body').popover({ selector: '[data-toggle=popover]',
- trigger: 'click hover',
- content: function() {
- return this.getAttribute("content") +
- self.getShortcut(this.getAttribute("shortcut"));
- }
- });
- },
- /**
- *
- * @param id indicates the popover associated with the shortcut
- * @returns {string} the keyboard shortcut used for the id given
- */
- getShortcut: function (id) {
- for (var keycode in shortcuts) {
- if (shortcuts.hasOwnProperty(keycode)) {
- if (shortcuts[keycode].id === id) {
- return " (" + shortcuts[keycode].character + ")";
- }
- }
- }
- return "";
- }
-};
-
-module.exports = KeyboardShortcut;
-
-},{}],38:[function(require,module,exports){
-var email = '';
-var displayName = '';
-var userId;
-var language = null;
-
-
-function supportsLocalStorage() {
- try {
- return 'localStorage' in window && window.localStorage !== null;
- } catch (e) {
- console.log("localstorage is not supported");
- return false;
- }
-}
-
-
-function generateUniqueId() {
- function _p8() {
- return (Math.random().toString(16) + "000000000").substr(2, 8);
- }
- return _p8() + _p8() + _p8() + _p8();
-}
-
-if (supportsLocalStorage()) {
- if (!window.localStorage.jitsiMeetId) {
- window.localStorage.jitsiMeetId = generateUniqueId();
- console.log("generated id", window.localStorage.jitsiMeetId);
- }
- userId = window.localStorage.jitsiMeetId || '';
- email = window.localStorage.email || '';
- displayName = window.localStorage.displayname || '';
- language = window.localStorage.language;
-} else {
- console.log("local storage is not supported");
- userId = generateUniqueId();
-}
-
-var Settings =
-{
- setDisplayName: function (newDisplayName) {
- displayName = newDisplayName;
- window.localStorage.displayname = displayName;
- return displayName;
- },
- setEmail: function (newEmail)
- {
- email = newEmail;
- window.localStorage.email = newEmail;
- return email;
- },
- getSettings: function () {
- return {
- email: email,
- displayName: displayName,
- uid: userId,
- language: language
- };
- },
- setLanguage: function (lang) {
- language = lang;
- window.localStorage.language = lang;
- }
-};
-
-module.exports = Settings;
+},{"../../service/desktopsharing/DesktopSharingEventTypes":96,"events":1}],38:[function(require,module,exports){
+//maps keycode to character, id of popover for given function and function
+var shortcuts = {
+ 67: {
+ character: "C",
+ id: "toggleChatPopover",
+ function: APP.UI.toggleChat
+ },
+ 70: {
+ character: "F",
+ id: "filmstripPopover",
+ function: APP.UI.toggleFilmStrip
+ },
+ 77: {
+ character: "M",
+ id: "mutePopover",
+ function: APP.UI.toggleAudio
+ },
+ 84: {
+ character: "T",
+ function: function() {
+ if(!APP.RTC.localAudio.isMuted()) {
+ APP.UI.toggleAudio();
+ }
+ }
+ },
+ 86: {
+ character: "V",
+ id: "toggleVideoPopover",
+ function: APP.UI.toggleVideo
+ }
+};
+
+
+var KeyboardShortcut = {
+ init: function () {
+ window.onkeyup = function(e) {
+ var keycode = e.which;
+ if(!($(":focus").is("input[type=text]") ||
+ $(":focus").is("input[type=password]") ||
+ $(":focus").is("textarea"))) {
+ if (typeof shortcuts[keycode] === "object") {
+ shortcuts[keycode].function();
+ }
+ else if (keycode >= "0".charCodeAt(0) &&
+ keycode <= "9".charCodeAt(0)) {
+ APP.UI.clickOnVideo(keycode - "0".charCodeAt(0) + 1);
+ }
+ //esc while the smileys are visible hides them
+ } else if (keycode === 27 && $('#smileysContainer').is(':visible')) {
+ APP.UI.toggleSmileys();
+ }
+ };
+
+ window.onkeydown = function(e) {
+ if(!($(":focus").is("input[type=text]") ||
+ $(":focus").is("input[type=password]") ||
+ $(":focus").is("textarea"))) {
+ if(e.which === "T".charCodeAt(0)) {
+ if(APP.RTC.localAudio.isMuted()) {
+ APP.UI.toggleAudio();
+ }
+ }
+ }
+ };
+ var self = this;
+ $('body').popover({ selector: '[data-toggle=popover]',
+ trigger: 'click hover',
+ content: function() {
+ return this.getAttribute("content") +
+ self.getShortcut(this.getAttribute("shortcut"));
+ }
+ });
+ },
+ /**
+ *
+ * @param id indicates the popover associated with the shortcut
+ * @returns {string} the keyboard shortcut used for the id given
+ */
+ getShortcut: function (id) {
+ for (var keycode in shortcuts) {
+ if (shortcuts.hasOwnProperty(keycode)) {
+ if (shortcuts[keycode].id === id) {
+ return " (" + shortcuts[keycode].character + ")";
+ }
+ }
+ }
+ return "";
+ }
+};
+
+module.exports = KeyboardShortcut;
},{}],39:[function(require,module,exports){
-/**
- *
- * @constructor
- */
-function SimulcastLogger(name, lvl) {
- this.name = name;
- this.lvl = lvl;
-}
+var email = '';
+var displayName = '';
+var userId;
+var language = null;
+
+
+function supportsLocalStorage() {
+ try {
+ return 'localStorage' in window && window.localStorage !== null;
+ } catch (e) {
+ console.log("localstorage is not supported");
+ return false;
+ }
+}
+
+
+function generateUniqueId() {
+ function _p8() {
+ return (Math.random().toString(16) + "000000000").substr(2, 8);
+ }
+ return _p8() + _p8() + _p8() + _p8();
+}
+
+if (supportsLocalStorage()) {
+ if (!window.localStorage.jitsiMeetId) {
+ window.localStorage.jitsiMeetId = generateUniqueId();
+ console.log("generated id", window.localStorage.jitsiMeetId);
+ }
+ userId = window.localStorage.jitsiMeetId || '';
+ email = window.localStorage.email || '';
+ displayName = window.localStorage.displayname || '';
+ language = window.localStorage.language;
+} else {
+ console.log("local storage is not supported");
+ userId = generateUniqueId();
+}
+
+var Settings =
+{
+ setDisplayName: function (newDisplayName) {
+ displayName = newDisplayName;
+ window.localStorage.displayname = displayName;
+ return displayName;
+ },
+ setEmail: function (newEmail)
+ {
+ email = newEmail;
+ window.localStorage.email = newEmail;
+ return email;
+ },
+ getSettings: function () {
+ return {
+ email: email,
+ displayName: displayName,
+ uid: userId,
+ language: language
+ };
+ },
+ setLanguage: function (lang) {
+ language = lang;
+ window.localStorage.language = lang;
+ }
+};
+
+module.exports = Settings;
-SimulcastLogger.prototype.log = function (text) {
- if (this.lvl) {
- console.log(text);
- }
-};
-
-SimulcastLogger.prototype.info = function (text) {
- if (this.lvl > 1) {
- console.info(text);
- }
-};
-
-SimulcastLogger.prototype.fine = function (text) {
- if (this.lvl > 2) {
- console.log(text);
- }
-};
-
-SimulcastLogger.prototype.error = function (text) {
- console.error(text);
-};
-
-module.exports = SimulcastLogger;
},{}],40:[function(require,module,exports){
-var SimulcastLogger = require("./SimulcastLogger");
-var SimulcastUtils = require("./SimulcastUtils");
-var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
-
-function SimulcastReceiver() {
- this.simulcastUtils = new SimulcastUtils();
- this.logger = new SimulcastLogger('SimulcastReceiver', 1);
-}
-
-SimulcastReceiver.prototype._remoteVideoSourceCache = '';
-SimulcastReceiver.prototype._remoteMaps = {
- msid2Quality: {},
- ssrc2Msid: {},
- msid2ssrc: {},
- receivingVideoStreams: {}
-};
-
-SimulcastReceiver.prototype._cacheRemoteVideoSources = function (lines) {
- this._remoteVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
-};
-
-SimulcastReceiver.prototype._restoreRemoteVideoSources = function (lines) {
- this.simulcastUtils._replaceVideoSources(lines, this._remoteVideoSourceCache);
-};
-
-SimulcastReceiver.prototype._ensureGoogConference = function (lines) {
- var sb;
-
- this.logger.info('Ensuring x-google-conference flag...')
-
- if (this.simulcastUtils._indexOfArray('a=x-google-flag:conference', lines) === this.simulcastUtils._emptyCompoundIndex) {
- // TODO(gp) do that for the audio as well as suggested by fippo.
- // Add the google conference flag
- sb = this.simulcastUtils._getVideoSources(lines);
- sb = ['a=x-google-flag:conference'].concat(sb);
- this.simulcastUtils._replaceVideoSources(lines, sb);
- }
-};
-
-SimulcastReceiver.prototype._restoreSimulcastGroups = function (sb) {
- this._restoreRemoteVideoSources(sb);
-};
-
-/**
- * Restores the simulcast groups of the remote description. In
- * transformRemoteDescription we remove those in order for the set remote
- * description to succeed. The focus needs the signal the groups to new
- * participants.
- *
- * @param desc
- * @returns {*}
- */
-SimulcastReceiver.prototype.reverseTransformRemoteDescription = function (desc) {
- var sb;
-
- if (!this.simulcastUtils.isValidDescription(desc)) {
- return desc;
- }
-
- if (config.enableSimulcast) {
- sb = desc.sdp.split('\r\n');
-
- this._restoreSimulcastGroups(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
- }
-
- return desc;
-};
-
-SimulcastUtils.prototype._ensureOrder = function (lines) {
- var videoSources, sb;
-
- videoSources = this.parseMedia(lines, ['video'])[0];
- sb = this._compileVideoSources(videoSources);
-
- this._replaceVideoSources(lines, sb);
-};
-
-SimulcastReceiver.prototype._updateRemoteMaps = function (lines) {
- var remoteVideoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0],
- videoSource, quality;
-
- // (re) initialize the remote maps.
- this._remoteMaps.msid2Quality = {};
- this._remoteMaps.ssrc2Msid = {};
- this._remoteMaps.msid2ssrc = {};
-
- var self = this;
- if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
- remoteVideoSources.groups.forEach(function (group) {
- if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
- quality = 0;
- group.ssrcs.forEach(function (ssrc) {
- videoSource = remoteVideoSources.sources[ssrc];
- self._remoteMaps.msid2Quality[videoSource.msid] = quality++;
- self._remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
- self._remoteMaps.msid2ssrc[videoSource.msid] = videoSource.ssrc;
- });
- }
- });
- }
-};
-
-SimulcastReceiver.prototype._setReceivingVideoStream = function (resource, ssrc) {
- this._remoteMaps.receivingVideoStreams[resource] = ssrc;
-};
-
-/**
- * Returns a stream with single video track, the one currently being
- * received by this endpoint.
- *
- * @param stream the remote simulcast stream.
- * @returns {webkitMediaStream}
- */
-SimulcastReceiver.prototype.getReceivingVideoStream = function (stream) {
- var tracks, i, electedTrack, msid, quality = 0, receivingTrackId;
-
- var self = this;
- if (config.enableSimulcast) {
-
- stream.getVideoTracks().some(function (track) {
- return Object.keys(self._remoteMaps.receivingVideoStreams).some(function (resource) {
- var ssrc = self._remoteMaps.receivingVideoStreams[resource];
- var msid = self._remoteMaps.ssrc2Msid[ssrc];
- if (msid == [stream.id, track.id].join(' ')) {
- electedTrack = track;
- return true;
- }
- });
- });
-
- if (!electedTrack) {
- // we don't have an elected track, choose by initial quality.
- tracks = stream.getVideoTracks();
- for (i = 0; i < tracks.length; i++) {
- msid = [stream.id, tracks[i].id].join(' ');
- if (this._remoteMaps.msid2Quality[msid] === quality) {
- electedTrack = tracks[i];
- break;
- }
- }
-
- // TODO(gp) if the initialQuality could not be satisfied, lower
- // the requirement and try again.
- }
- }
-
- return (electedTrack)
- ? new webkitMediaStream([electedTrack])
- : stream;
-};
-
-SimulcastReceiver.prototype.getReceivingSSRC = function (jid) {
- var resource = Strophe.getResourceFromJid(jid);
- var ssrc = this._remoteMaps.receivingVideoStreams[resource];
-
- // If we haven't receiving a "changed" event yet, then we must be receiving
- // low quality (that the sender always streams).
- if(!ssrc)
- {
- var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
- var remoteStream = remoteStreamObject.getOriginalStream();
- var tracks = remoteStream.getVideoTracks();
- if (tracks) {
- for (var k = 0; k < tracks.length; k++) {
- var track = tracks[k];
- var msid = [remoteStream.id, track.id].join(' ');
- var _ssrc = this._remoteMaps.msid2ssrc[msid];
- var quality = this._remoteMaps.msid2Quality[msid];
- if (quality == 0) {
- ssrc = _ssrc;
- }
- }
- }
- }
-
- return ssrc;
-};
-
-SimulcastReceiver.prototype.getReceivingVideoStreamBySSRC = function (ssrc)
-{
- var sid, electedStream;
- var i, j, k;
- var jid = APP.xmpp.getJidFromSSRC(ssrc);
- if(jid && APP.RTC.remoteStreams[jid])
- {
- var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
- var remoteStream = remoteStreamObject.getOriginalStream();
- var tracks = remoteStream.getVideoTracks();
- if (tracks) {
- for (k = 0; k < tracks.length; k++) {
- var track = tracks[k];
- var msid = [remoteStream.id, track.id].join(' ');
- var tmp = this._remoteMaps.msid2ssrc[msid];
- if (tmp == ssrc) {
- electedStream = new webkitMediaStream([track]);
- sid = remoteStreamObject.sid;
- // stream found, stop.
- break;
- }
- }
- }
-
- }
- else
- {
- console.debug(APP.RTC.remoteStreams, jid, ssrc);
- }
-
- return {
- sid: sid,
- stream: electedStream
- };
-};
-
-/**
- * Gets the fully qualified msid (stream.id + track.id) associated to the
- * SSRC.
- *
- * @param ssrc
- * @returns {*}
- */
-SimulcastReceiver.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
- return this._remoteMaps.ssrc2Msid[ssrc];
-};
-
-/**
- * Removes the ssrc-group:SIM from the remote description bacause Chrome
- * either gets confused and thinks this is an FID group or, if an FID group
- * is already present, it fails to set the remote description.
- *
- * @param desc
- * @returns {*}
- */
-SimulcastReceiver.prototype.transformRemoteDescription = function (desc) {
-
- if (desc && desc.sdp) {
- var sb = desc.sdp.split('\r\n');
-
- this._updateRemoteMaps(sb);
- this._cacheRemoteVideoSources(sb);
-
- // NOTE(gp) this needs to be called after updateRemoteMaps because we
- // need the simulcast group in the _updateRemoteMaps() method.
- this.simulcastUtils._removeSimulcastGroup(sb);
-
- if (desc.sdp.indexOf('a=ssrc-group:SIM') !== -1) {
- // We don't need the goog conference flag if we're not doing
- // simulcast.
- this._ensureGoogConference(sb);
- }
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine(['Transformed remote description', desc.sdp].join(' '));
- }
-
- return desc;
-};
-
+/**
+ *
+ * @constructor
+ */
+function SimulcastLogger(name, lvl) {
+ this.name = name;
+ this.lvl = lvl;
+}
+
+SimulcastLogger.prototype.log = function (text) {
+ if (this.lvl) {
+ console.log(text);
+ }
+};
+
+SimulcastLogger.prototype.info = function (text) {
+ if (this.lvl > 1) {
+ console.info(text);
+ }
+};
+
+SimulcastLogger.prototype.fine = function (text) {
+ if (this.lvl > 2) {
+ console.log(text);
+ }
+};
+
+SimulcastLogger.prototype.error = function (text) {
+ console.error(text);
+};
+
+module.exports = SimulcastLogger;
+},{}],41:[function(require,module,exports){
+var SimulcastLogger = require("./SimulcastLogger");
+var SimulcastUtils = require("./SimulcastUtils");
+var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
+
+function SimulcastReceiver() {
+ this.simulcastUtils = new SimulcastUtils();
+ this.logger = new SimulcastLogger('SimulcastReceiver', 1);
+}
+
+SimulcastReceiver.prototype._remoteVideoSourceCache = '';
+SimulcastReceiver.prototype._remoteMaps = {
+ msid2Quality: {},
+ ssrc2Msid: {},
+ msid2ssrc: {},
+ receivingVideoStreams: {}
+};
+
+SimulcastReceiver.prototype._cacheRemoteVideoSources = function (lines) {
+ this._remoteVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
+};
+
+SimulcastReceiver.prototype._restoreRemoteVideoSources = function (lines) {
+ this.simulcastUtils._replaceVideoSources(lines, this._remoteVideoSourceCache);
+};
+
+SimulcastReceiver.prototype._ensureGoogConference = function (lines) {
+ var sb;
+
+ this.logger.info('Ensuring x-google-conference flag...')
+
+ if (this.simulcastUtils._indexOfArray('a=x-google-flag:conference', lines) === this.simulcastUtils._emptyCompoundIndex) {
+ // TODO(gp) do that for the audio as well as suggested by fippo.
+ // Add the google conference flag
+ sb = this.simulcastUtils._getVideoSources(lines);
+ sb = ['a=x-google-flag:conference'].concat(sb);
+ this.simulcastUtils._replaceVideoSources(lines, sb);
+ }
+};
+
+SimulcastReceiver.prototype._restoreSimulcastGroups = function (sb) {
+ this._restoreRemoteVideoSources(sb);
+};
+
+/**
+ * Restores the simulcast groups of the remote description. In
+ * transformRemoteDescription we remove those in order for the set remote
+ * description to succeed. The focus needs the signal the groups to new
+ * participants.
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastReceiver.prototype.reverseTransformRemoteDescription = function (desc) {
+ var sb;
+
+ if (!this.simulcastUtils.isValidDescription(desc)) {
+ return desc;
+ }
+
+ if (config.enableSimulcast) {
+ sb = desc.sdp.split('\r\n');
+
+ this._restoreSimulcastGroups(sb);
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+ }
+
+ return desc;
+};
+
+SimulcastUtils.prototype._ensureOrder = function (lines) {
+ var videoSources, sb;
+
+ videoSources = this.parseMedia(lines, ['video'])[0];
+ sb = this._compileVideoSources(videoSources);
+
+ this._replaceVideoSources(lines, sb);
+};
+
+SimulcastReceiver.prototype._updateRemoteMaps = function (lines) {
+ var remoteVideoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0],
+ videoSource, quality;
+
+ // (re) initialize the remote maps.
+ this._remoteMaps.msid2Quality = {};
+ this._remoteMaps.ssrc2Msid = {};
+ this._remoteMaps.msid2ssrc = {};
+
+ var self = this;
+ if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
+ remoteVideoSources.groups.forEach(function (group) {
+ if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
+ quality = 0;
+ group.ssrcs.forEach(function (ssrc) {
+ videoSource = remoteVideoSources.sources[ssrc];
+ self._remoteMaps.msid2Quality[videoSource.msid] = quality++;
+ self._remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
+ self._remoteMaps.msid2ssrc[videoSource.msid] = videoSource.ssrc;
+ });
+ }
+ });
+ }
+};
+
+SimulcastReceiver.prototype._setReceivingVideoStream = function (resource, ssrc) {
+ this._remoteMaps.receivingVideoStreams[resource] = ssrc;
+};
+
+/**
+ * Returns a stream with single video track, the one currently being
+ * received by this endpoint.
+ *
+ * @param stream the remote simulcast stream.
+ * @returns {webkitMediaStream}
+ */
+SimulcastReceiver.prototype.getReceivingVideoStream = function (stream) {
+ var tracks, i, electedTrack, msid, quality = 0, receivingTrackId;
+
+ var self = this;
+ if (config.enableSimulcast) {
+
+ stream.getVideoTracks().some(function (track) {
+ return Object.keys(self._remoteMaps.receivingVideoStreams).some(function (resource) {
+ var ssrc = self._remoteMaps.receivingVideoStreams[resource];
+ var msid = self._remoteMaps.ssrc2Msid[ssrc];
+ if (msid == [stream.id, track.id].join(' ')) {
+ electedTrack = track;
+ return true;
+ }
+ });
+ });
+
+ if (!electedTrack) {
+ // we don't have an elected track, choose by initial quality.
+ tracks = stream.getVideoTracks();
+ for (i = 0; i < tracks.length; i++) {
+ msid = [stream.id, tracks[i].id].join(' ');
+ if (this._remoteMaps.msid2Quality[msid] === quality) {
+ electedTrack = tracks[i];
+ break;
+ }
+ }
+
+ // TODO(gp) if the initialQuality could not be satisfied, lower
+ // the requirement and try again.
+ }
+ }
+
+ return (electedTrack)
+ ? new webkitMediaStream([electedTrack])
+ : stream;
+};
+
+SimulcastReceiver.prototype.getReceivingSSRC = function (jid) {
+ var resource = Strophe.getResourceFromJid(jid);
+ var ssrc = this._remoteMaps.receivingVideoStreams[resource];
+
+ // If we haven't receiving a "changed" event yet, then we must be receiving
+ // low quality (that the sender always streams).
+ if(!ssrc)
+ {
+ var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
+ var remoteStream = remoteStreamObject.getOriginalStream();
+ var tracks = remoteStream.getVideoTracks();
+ if (tracks) {
+ for (var k = 0; k < tracks.length; k++) {
+ var track = tracks[k];
+ var msid = [remoteStream.id, track.id].join(' ');
+ var _ssrc = this._remoteMaps.msid2ssrc[msid];
+ var quality = this._remoteMaps.msid2Quality[msid];
+ if (quality == 0) {
+ ssrc = _ssrc;
+ }
+ }
+ }
+ }
+
+ return ssrc;
+};
+
+SimulcastReceiver.prototype.getReceivingVideoStreamBySSRC = function (ssrc)
+{
+ var sid, electedStream;
+ var i, j, k;
+ var jid = APP.xmpp.getJidFromSSRC(ssrc);
+ if(jid && APP.RTC.remoteStreams[jid])
+ {
+ var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
+ var remoteStream = remoteStreamObject.getOriginalStream();
+ var tracks = remoteStream.getVideoTracks();
+ if (tracks) {
+ for (k = 0; k < tracks.length; k++) {
+ var track = tracks[k];
+ var msid = [remoteStream.id, track.id].join(' ');
+ var tmp = this._remoteMaps.msid2ssrc[msid];
+ if (tmp == ssrc) {
+ electedStream = new webkitMediaStream([track]);
+ sid = remoteStreamObject.sid;
+ // stream found, stop.
+ break;
+ }
+ }
+ }
+
+ }
+ else
+ {
+ console.debug(APP.RTC.remoteStreams, jid, ssrc);
+ }
+
+ return {
+ sid: sid,
+ stream: electedStream
+ };
+};
+
+/**
+ * Gets the fully qualified msid (stream.id + track.id) associated to the
+ * SSRC.
+ *
+ * @param ssrc
+ * @returns {*}
+ */
+SimulcastReceiver.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
+ return this._remoteMaps.ssrc2Msid[ssrc];
+};
+
+/**
+ * Removes the ssrc-group:SIM from the remote description bacause Chrome
+ * either gets confused and thinks this is an FID group or, if an FID group
+ * is already present, it fails to set the remote description.
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastReceiver.prototype.transformRemoteDescription = function (desc) {
+
+ if (desc && desc.sdp) {
+ var sb = desc.sdp.split('\r\n');
+
+ this._updateRemoteMaps(sb);
+ this._cacheRemoteVideoSources(sb);
+
+ // NOTE(gp) this needs to be called after updateRemoteMaps because we
+ // need the simulcast group in the _updateRemoteMaps() method.
+ this.simulcastUtils._removeSimulcastGroup(sb);
+
+ if (desc.sdp.indexOf('a=ssrc-group:SIM') !== -1) {
+ // We don't need the goog conference flag if we're not doing
+ // simulcast.
+ this._ensureGoogConference(sb);
+ }
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+
+ this.logger.fine(['Transformed remote description', desc.sdp].join(' '));
+ }
+
+ return desc;
+};
+
module.exports = SimulcastReceiver;
-},{"../../service/RTC/MediaStreamTypes":87,"./SimulcastLogger":39,"./SimulcastUtils":42}],41:[function(require,module,exports){
-var SimulcastLogger = require("./SimulcastLogger");
-var SimulcastUtils = require("./SimulcastUtils");
-
-function SimulcastSender() {
- this.simulcastUtils = new SimulcastUtils();
- this.logger = new SimulcastLogger('SimulcastSender', 1);
-}
-
-SimulcastSender.prototype.displayedLocalVideoStream = null;
-
-SimulcastSender.prototype._generateGuid = (function () {
- function s4() {
- return Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
- }
-
- return function () {
- return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
- s4() + '-' + s4() + s4() + s4();
- };
-}());
-
-// Returns a random integer between min (included) and max (excluded)
-// Using Math.round() gives a non-uniform distribution!
-SimulcastSender.prototype._generateRandomSSRC = function () {
- var min = 0, max = 0xffffffff;
- return Math.floor(Math.random() * (max - min)) + min;
-};
-
-SimulcastSender.prototype.getLocalVideoStream = function () {
- return (this.displayedLocalVideoStream != null)
- ? this.displayedLocalVideoStream
- // in case we have no simulcast at all, i.e. we didn't perform the GUM
- : APP.RTC.localVideo.getOriginalStream();
-};
-
-function NativeSimulcastSender() {
- SimulcastSender.call(this); // call the super constructor.
-}
-
-NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
-NativeSimulcastSender.prototype._localExplosionMap = {};
-NativeSimulcastSender.prototype._isUsingScreenStream = false;
-NativeSimulcastSender.prototype._localVideoSourceCache = '';
-
-NativeSimulcastSender.prototype.reset = function () {
- this._localExplosionMap = {};
- this._isUsingScreenStream = APP.desktopsharing.isUsingScreenStream();
-};
-
-NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
- this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
-};
-
-NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
- this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
-};
-
-NativeSimulcastSender.prototype._appendSimulcastGroup = function (lines) {
- var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
-
- this.logger.info('Appending simulcast group...');
-
- // Get the primary SSRC information.
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- // Start building the SIM SSRC group.
- ssrcGroup = ['a=ssrc-group:SIM'];
-
- // The video source buffer.
- sb = [];
-
- // Create the simulcast sub-streams.
- for (i = 0; i < numOfSubs; i++) {
- // TODO(gp) prevent SSRC collision.
- simSSRC = this._generateRandomSSRC();
- ssrcGroup.push(simSSRC);
-
- if (videoSources.base) {
- sb.splice.apply(sb, [sb.length, 0].concat(
- [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
- ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
- ));
- }
-
- this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
-
- }
-
- // Add the group sim layers.
- sb.splice(0, 0, ssrcGroup.join(' '))
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
-};
-
-// Does the actual patching.
-NativeSimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
-
- this.logger.info('Ensuring simulcast group...');
-
- if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) {
- this._appendSimulcastGroup(lines);
- this._cacheLocalVideoSources(lines);
- } else {
- // verify that the ssrcs participating in the SIM group are present
- // in the SDP (needed for presence).
- this._restoreLocalVideoSources(lines);
- }
-};
-
-/**
- * Produces a single stream with multiple tracks for local video sources.
- *
- * @param lines
- * @private
- */
-NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) {
- var sb, msid, sid, tid, videoSources, self;
-
- this.logger.info('Exploding local video sources...');
-
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- self = this;
- if (videoSources.groups && videoSources.groups.length !== 0) {
- videoSources.groups.forEach(function (group) {
- if (group.semantics === 'SIM') {
- group.ssrcs.forEach(function (ssrc) {
-
- // Get the msid for this ssrc..
- if (self._localExplosionMap[ssrc]) {
- // .. either from the explosion map..
- msid = self._localExplosionMap[ssrc];
- } else {
- // .. or generate a new one (msid).
- sid = videoSources.sources[ssrc].msid
- .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
-
- tid = self._generateGuid();
- msid = [sid, tid].join(' ');
- self._localExplosionMap[ssrc] = msid;
- }
-
- // Assign it to the source object.
- videoSources.sources[ssrc].msid = msid;
-
- // TODO(gp) Change the msid of associated sources.
- });
- }
- });
- }
-
- sb = this.simulcastUtils._compileVideoSources(videoSources);
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
-};
-
-/**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
-NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
-
- // There's nothing special to do for native simulcast, so just do a normal GUM.
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
- success(hqStream);
- }, err);
-};
-
-/**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
-NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- var sb;
-
- if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
- return desc;
- }
-
-
- sb = desc.sdp.split('\r\n');
-
- this._explodeSimulcastSenderSources(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine(['Exploded local video sources', desc.sdp].join(' '));
-
- return desc;
-};
-
-/**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
-NativeSimulcastSender.prototype.transformAnswer = function (desc) {
-
- if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
- return desc;
- }
-
- var sb = desc.sdp.split('\r\n');
-
- // Even if we have enabled native simulcasting previously
- // (with a call to SLD with an appropriate SDP, for example),
- // createAnswer seems to consistently generate incomplete SDP
- // with missing SSRCS.
- //
- // So, subsequent calls to SLD will have missing SSRCS and presence
- // won't have the complete list of SRCs.
- this._ensureSimulcastGroup(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine(['Transformed answer', desc.sdp].join(' '));
-
- return desc;
-};
-
-
-/**
- *
- *
- * @param desc
- * @returns {*}
- */
-NativeSimulcastSender.prototype.transformLocalDescription = function (desc) {
- return desc;
-};
-
-NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
- // Nothing to do here, native simulcast does that auto-magically.
-};
-
-NativeSimulcastSender.prototype.constructor = NativeSimulcastSender;
-
-function SimpleSimulcastSender() {
- SimulcastSender.call(this);
-}
-
-SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
-SimpleSimulcastSender.prototype.localStream = null;
-SimpleSimulcastSender.prototype._localMaps = {
- msids: [],
- msid2ssrc: {}
-};
-
-/**
- * Groups local video sources together in the ssrc-group:SIM group.
- *
- * @param lines
- * @private
- */
-SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) {
- var sb, videoSources, ssrcs = [], ssrc;
-
- this.logger.info('Grouping local video sources...');
-
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- for (ssrc in videoSources.sources) {
- // jitsi-meet destroys/creates streams at various places causing
- // the original local stream ids to change. The only thing that
- // remains unchanged is the trackid.
- this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
- }
-
- var self = this;
- // TODO(gp) add only "free" sources.
- this._localMaps.msids.forEach(function (msid) {
- ssrcs.push(self._localMaps.msid2ssrc[msid]);
- });
-
- if (!videoSources.groups) {
- videoSources.groups = [];
- }
-
- videoSources.groups.push({
- 'semantics': 'SIM',
- 'ssrcs': ssrcs
- });
-
- sb = this.simulcastUtils._compileVideoSources(videoSources);
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
-};
-
-/**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
-SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
-
- // TODO(gp) what if we request a resolution not supported by the hardware?
- // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
- var lqConstraints = {
- audio: false,
- video: {
- mandatory: {
- maxWidth: 320,
- maxHeight: 180,
- maxFrameRate: 15
- }
- }
- };
-
- this.logger.info('HQ constraints: ', constraints);
- this.logger.info('LQ constraints: ', lqConstraints);
-
-
- // NOTE(gp) if we request the lq stream first webkitGetUserMedia
- // fails randomly. Tested with Chrome 37. As fippo suggested, the
- // reason appears to be that Chrome only acquires the cam once and
- // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
-
- var self = this;
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
-
- self.localStream = hqStream;
-
- // reset local maps.
- self._localMaps.msids = [];
- self._localMaps.msid2ssrc = {};
-
- // add hq trackid to local map
- self._localMaps.msids.push(hqStream.getVideoTracks()[0].id);
-
- navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
-
- self.displayedLocalVideoStream = lqStream;
-
- // NOTE(gp) The specification says Array.forEach() will visit
- // the array elements in numeric order, and that it doesn't
- // visit elements that don't exist.
-
- // add lq trackid to local map
- self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
-
- self.localStream.addTrack(lqStream.getVideoTracks()[0]);
- success(self.localStream);
- }, err);
- }, err);
-};
-
-/**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
-SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- var sb;
-
- if (!this.simulcastUtils.isValidDescription(desc)) {
- return desc;
- }
-
- sb = desc.sdp.split('\r\n');
-
- this._groupLocalVideoSources(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine('Grouped local video sources');
- this.logger.fine(desc.sdp);
-
- return desc;
-};
-
-/**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
-SimpleSimulcastSender.prototype.transformAnswer = function (desc) {
- return desc;
-};
-
-
-/**
- *
- *
- * @param desc
- * @returns {*}
- */
-SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) {
-
- var sb = desc.sdp.split('\r\n');
-
- this.simulcastUtils._removeSimulcastGroup(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine('Transformed local description');
- this.logger.fine(desc.sdp);
-
- return desc;
-};
-
-SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
- var trackid;
-
- var self = this;
- this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
- if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) {
- // Search for the track id that corresponds to the ssrc
- if (self._localMaps.msid2ssrc[tid] == ssrc) {
- trackid = tid;
- return true;
- }
- }) && self.localStream.getVideoTracks().some(function (track) {
- // Start/stop the track that corresponds to the track id
- if (track.id === trackid) {
- track.enabled = enabled;
- return true;
- }
- })) {
- this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
- $(document).trigger(enabled
- ? 'simulcastlayerstarted'
- : 'simulcastlayerstopped');
- } else {
- this.logger.error("I don't have a local stream with SSRC " + ssrc);
- }
-};
-
-SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender;
-
-function NoSimulcastSender() {
- SimulcastSender.call(this);
-}
-
-NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
-/**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
-NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
- success(hqStream);
- }, err);
-};
-
-/**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
-NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- return desc;
-};
-
-/**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
-NoSimulcastSender.prototype.transformAnswer = function (desc) {
- return desc;
-};
-
-
-/**
- *
- *
- * @param desc
- * @returns {*}
- */
-NoSimulcastSender.prototype.transformLocalDescription = function (desc) {
- return desc;
-};
-
-NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
-
-};
-
-NoSimulcastSender.prototype.constructor = NoSimulcastSender;
-
-module.exports = {
- "native": NativeSimulcastSender,
- "no": NoSimulcastSender
-}
-
-},{"./SimulcastLogger":39,"./SimulcastUtils":42}],42:[function(require,module,exports){
-var SimulcastLogger = require("./SimulcastLogger");
-
-/**
- *
- * @constructor
- */
-function SimulcastUtils() {
- this.logger = new SimulcastLogger("SimulcastUtils", 1);
-}
-
-/**
- *
- * @type {{}}
- * @private
- */
-SimulcastUtils.prototype._emptyCompoundIndex = {};
-
-/**
- *
- * @param lines
- * @param videoSources
- * @private
- */
-SimulcastUtils.prototype._replaceVideoSources = function (lines, videoSources) {
- var i, inVideo = false, index = -1, howMany = 0;
-
- this.logger.info('Replacing video sources...');
-
- for (i = 0; i < lines.length; i++) {
- if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
- // Out of video.
- break;
- }
-
- if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
- // In video.
- inVideo = true;
- }
-
- if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
- || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
-
- if (index === -1) {
- index = i;
- }
-
- howMany++;
- }
- }
-
- // efficiency baby ;)
- lines.splice.apply(lines,
- [index, howMany].concat(videoSources));
-
-};
-
-SimulcastUtils.prototype.isValidDescription = function (desc)
-{
- return desc && desc != null
- && desc.type && desc.type != ''
- && desc.sdp && desc.sdp != '';
-};
-
-SimulcastUtils.prototype._getVideoSources = function (lines) {
- var i, inVideo = false, sb = [];
-
- this.logger.info('Getting video sources...');
-
- for (i = 0; i < lines.length; i++) {
- if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
- // Out of video.
- break;
- }
-
- if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
- // In video.
- inVideo = true;
- }
-
- if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
- // In SSRC.
- sb.push(lines[i]);
- }
-
- if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
- sb.push(lines[i]);
- }
- }
-
- return sb;
-};
-
-SimulcastUtils.prototype.parseMedia = function (lines, mediatypes) {
- var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
- ssrc_attribute, group, semantics, skip = true;
-
- this.logger.info('Parsing media sources...');
-
- for (i = 0; i < lines.length; i++) {
- if (lines[i].substring(0, 'm='.length) === 'm=') {
-
- type = lines[i]
- .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
- skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
-
- if (!skip) {
- cur_media = {
- 'type': type,
- 'sources': {},
- 'groups': []
- };
-
- res.push(cur_media);
- }
-
- } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
-
- idx = lines[i].indexOf(' ');
- ssrc = lines[i].substring('a=ssrc:'.length, idx);
- if (cur_media.sources[ssrc] === undefined) {
- cur_ssrc = {'ssrc': ssrc};
- cur_media.sources[ssrc] = cur_ssrc;
- }
-
- ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
- cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
-
- if (cur_media.base === undefined) {
- cur_media.base = cur_ssrc;
- }
-
- } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
- idx = lines[i].indexOf(' ');
- semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
- ssrcs = lines[i].substr(idx).trim().split(' ');
- group = {
- 'semantics': semantics,
- 'ssrcs': ssrcs
- };
- cur_media.groups.push(group);
- } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
- lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
- lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
- lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
-
- cur_media.direction = lines[i].substring('a='.length);
- }
- }
-
- return res;
-};
-
-/**
- * The _indexOfArray() method returns the first a CompoundIndex at which a
- * given element can be found in the array, or _emptyCompoundIndex if it is
- * not present.
- *
- * Example:
- *
- * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ])
- *
- * returns {row: 2, column: 14}
- *
- * @param needle
- * @param haystack
- * @param start
- * @returns {}
- * @private
- */
-SimulcastUtils.prototype._indexOfArray = function (needle, haystack, start) {
- var length = haystack.length, idx, i;
-
- if (!start) {
- start = 0;
- }
-
- for (i = start; i < length; i++) {
- idx = haystack[i].indexOf(needle);
- if (idx !== -1) {
- return {row: i, column: idx};
- }
- }
- return this._emptyCompoundIndex;
-};
-
-SimulcastUtils.prototype._removeSimulcastGroup = function (lines) {
- var i;
-
- for (i = lines.length - 1; i >= 0; i--) {
- if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
- lines.splice(i, 1);
- }
- }
-};
-
-SimulcastUtils.prototype._compileVideoSources = function (videoSources) {
- var sb = [], ssrc, addedSSRCs = [];
-
- this.logger.info('Compiling video sources...');
-
- // Add the groups
- if (videoSources.groups && videoSources.groups.length !== 0) {
- videoSources.groups.forEach(function (group) {
- if (group.ssrcs && group.ssrcs.length !== 0) {
- sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
-
- // if (group.semantics !== 'SIM') {
- group.ssrcs.forEach(function (ssrc) {
- addedSSRCs.push(ssrc);
- sb.splice.apply(sb, [sb.length, 0].concat([
- ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
- ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
- });
- //}
- }
- });
- }
-
- // Then add any free sources.
- if (videoSources.sources) {
- for (ssrc in videoSources.sources) {
- if (addedSSRCs.indexOf(ssrc) === -1) {
- sb.splice.apply(sb, [sb.length, 0].concat([
- ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
- ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
- }
- }
- }
-
- return sb;
-};
-
+},{"../../service/RTC/MediaStreamTypes":88,"./SimulcastLogger":40,"./SimulcastUtils":43}],42:[function(require,module,exports){
+var SimulcastLogger = require("./SimulcastLogger");
+var SimulcastUtils = require("./SimulcastUtils");
+
+function SimulcastSender() {
+ this.simulcastUtils = new SimulcastUtils();
+ this.logger = new SimulcastLogger('SimulcastSender', 1);
+}
+
+SimulcastSender.prototype.displayedLocalVideoStream = null;
+
+SimulcastSender.prototype._generateGuid = (function () {
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+
+ return function () {
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+ s4() + '-' + s4() + s4() + s4();
+ };
+}());
+
+// Returns a random integer between min (included) and max (excluded)
+// Using Math.round() gives a non-uniform distribution!
+SimulcastSender.prototype._generateRandomSSRC = function () {
+ var min = 0, max = 0xffffffff;
+ return Math.floor(Math.random() * (max - min)) + min;
+};
+
+SimulcastSender.prototype.getLocalVideoStream = function () {
+ return (this.displayedLocalVideoStream != null)
+ ? this.displayedLocalVideoStream
+ // in case we have no simulcast at all, i.e. we didn't perform the GUM
+ : APP.RTC.localVideo.getOriginalStream();
+};
+
+function NativeSimulcastSender() {
+ SimulcastSender.call(this); // call the super constructor.
+}
+
+NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
+
+NativeSimulcastSender.prototype._localExplosionMap = {};
+NativeSimulcastSender.prototype._isUsingScreenStream = false;
+NativeSimulcastSender.prototype._localVideoSourceCache = '';
+
+NativeSimulcastSender.prototype.reset = function () {
+ this._localExplosionMap = {};
+ this._isUsingScreenStream = APP.desktopsharing.isUsingScreenStream();
+};
+
+NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
+ this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
+};
+
+NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
+ this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
+};
+
+NativeSimulcastSender.prototype._appendSimulcastGroup = function (lines) {
+ var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
+
+ this.logger.info('Appending simulcast group...');
+
+ // Get the primary SSRC information.
+ videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
+
+ // Start building the SIM SSRC group.
+ ssrcGroup = ['a=ssrc-group:SIM'];
+
+ // The video source buffer.
+ sb = [];
+
+ // Create the simulcast sub-streams.
+ for (i = 0; i < numOfSubs; i++) {
+ // TODO(gp) prevent SSRC collision.
+ simSSRC = this._generateRandomSSRC();
+ ssrcGroup.push(simSSRC);
+
+ if (videoSources.base) {
+ sb.splice.apply(sb, [sb.length, 0].concat(
+ [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
+ ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
+ ));
+ }
+
+ this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
+
+ }
+
+ // Add the group sim layers.
+ sb.splice(0, 0, ssrcGroup.join(' '))
+
+ this.simulcastUtils._replaceVideoSources(lines, sb);
+};
+
+// Does the actual patching.
+NativeSimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
+
+ this.logger.info('Ensuring simulcast group...');
+
+ if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) {
+ this._appendSimulcastGroup(lines);
+ this._cacheLocalVideoSources(lines);
+ } else {
+ // verify that the ssrcs participating in the SIM group are present
+ // in the SDP (needed for presence).
+ this._restoreLocalVideoSources(lines);
+ }
+};
+
+/**
+ * Produces a single stream with multiple tracks for local video sources.
+ *
+ * @param lines
+ * @private
+ */
+NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) {
+ var sb, msid, sid, tid, videoSources, self;
+
+ this.logger.info('Exploding local video sources...');
+
+ videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
+
+ self = this;
+ if (videoSources.groups && videoSources.groups.length !== 0) {
+ videoSources.groups.forEach(function (group) {
+ if (group.semantics === 'SIM') {
+ group.ssrcs.forEach(function (ssrc) {
+
+ // Get the msid for this ssrc..
+ if (self._localExplosionMap[ssrc]) {
+ // .. either from the explosion map..
+ msid = self._localExplosionMap[ssrc];
+ } else {
+ // .. or generate a new one (msid).
+ sid = videoSources.sources[ssrc].msid
+ .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
+
+ tid = self._generateGuid();
+ msid = [sid, tid].join(' ');
+ self._localExplosionMap[ssrc] = msid;
+ }
+
+ // Assign it to the source object.
+ videoSources.sources[ssrc].msid = msid;
+
+ // TODO(gp) Change the msid of associated sources.
+ });
+ }
+ });
+ }
+
+ sb = this.simulcastUtils._compileVideoSources(videoSources);
+
+ this.simulcastUtils._replaceVideoSources(lines, sb);
+};
+
+/**
+ * GUM for simulcast.
+ *
+ * @param constraints
+ * @param success
+ * @param err
+ */
+NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
+
+ // There's nothing special to do for native simulcast, so just do a normal GUM.
+ navigator.webkitGetUserMedia(constraints, function (hqStream) {
+ success(hqStream);
+ }, err);
+};
+
+/**
+ * Prepares the local description for public usage (i.e. to be signaled
+ * through Jingle to the focus).
+ *
+ * @param desc
+ * @returns {RTCSessionDescription}
+ */
+NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
+ var sb;
+
+ if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
+ return desc;
+ }
+
+
+ sb = desc.sdp.split('\r\n');
+
+ this._explodeSimulcastSenderSources(sb);
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+
+ this.logger.fine(['Exploded local video sources', desc.sdp].join(' '));
+
+ return desc;
+};
+
+/**
+ * Ensures that the simulcast group is present in the answer, _if_ native
+ * simulcast is enabled,
+ *
+ * @param desc
+ * @returns {*}
+ */
+NativeSimulcastSender.prototype.transformAnswer = function (desc) {
+
+ if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
+ return desc;
+ }
+
+ var sb = desc.sdp.split('\r\n');
+
+ // Even if we have enabled native simulcasting previously
+ // (with a call to SLD with an appropriate SDP, for example),
+ // createAnswer seems to consistently generate incomplete SDP
+ // with missing SSRCS.
+ //
+ // So, subsequent calls to SLD will have missing SSRCS and presence
+ // won't have the complete list of SRCs.
+ this._ensureSimulcastGroup(sb);
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+
+ this.logger.fine(['Transformed answer', desc.sdp].join(' '));
+
+ return desc;
+};
+
+
+/**
+ *
+ *
+ * @param desc
+ * @returns {*}
+ */
+NativeSimulcastSender.prototype.transformLocalDescription = function (desc) {
+ return desc;
+};
+
+NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
+ // Nothing to do here, native simulcast does that auto-magically.
+};
+
+NativeSimulcastSender.prototype.constructor = NativeSimulcastSender;
+
+function SimpleSimulcastSender() {
+ SimulcastSender.call(this);
+}
+
+SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
+
+SimpleSimulcastSender.prototype.localStream = null;
+SimpleSimulcastSender.prototype._localMaps = {
+ msids: [],
+ msid2ssrc: {}
+};
+
+/**
+ * Groups local video sources together in the ssrc-group:SIM group.
+ *
+ * @param lines
+ * @private
+ */
+SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) {
+ var sb, videoSources, ssrcs = [], ssrc;
+
+ this.logger.info('Grouping local video sources...');
+
+ videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
+
+ for (ssrc in videoSources.sources) {
+ // jitsi-meet destroys/creates streams at various places causing
+ // the original local stream ids to change. The only thing that
+ // remains unchanged is the trackid.
+ this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
+ }
+
+ var self = this;
+ // TODO(gp) add only "free" sources.
+ this._localMaps.msids.forEach(function (msid) {
+ ssrcs.push(self._localMaps.msid2ssrc[msid]);
+ });
+
+ if (!videoSources.groups) {
+ videoSources.groups = [];
+ }
+
+ videoSources.groups.push({
+ 'semantics': 'SIM',
+ 'ssrcs': ssrcs
+ });
+
+ sb = this.simulcastUtils._compileVideoSources(videoSources);
+
+ this.simulcastUtils._replaceVideoSources(lines, sb);
+};
+
+/**
+ * GUM for simulcast.
+ *
+ * @param constraints
+ * @param success
+ * @param err
+ */
+SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
+
+ // TODO(gp) what if we request a resolution not supported by the hardware?
+ // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
+ var lqConstraints = {
+ audio: false,
+ video: {
+ mandatory: {
+ maxWidth: 320,
+ maxHeight: 180,
+ maxFrameRate: 15
+ }
+ }
+ };
+
+ this.logger.info('HQ constraints: ', constraints);
+ this.logger.info('LQ constraints: ', lqConstraints);
+
+
+ // NOTE(gp) if we request the lq stream first webkitGetUserMedia
+ // fails randomly. Tested with Chrome 37. As fippo suggested, the
+ // reason appears to be that Chrome only acquires the cam once and
+ // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
+
+ var self = this;
+ navigator.webkitGetUserMedia(constraints, function (hqStream) {
+
+ self.localStream = hqStream;
+
+ // reset local maps.
+ self._localMaps.msids = [];
+ self._localMaps.msid2ssrc = {};
+
+ // add hq trackid to local map
+ self._localMaps.msids.push(hqStream.getVideoTracks()[0].id);
+
+ navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
+
+ self.displayedLocalVideoStream = lqStream;
+
+ // NOTE(gp) The specification says Array.forEach() will visit
+ // the array elements in numeric order, and that it doesn't
+ // visit elements that don't exist.
+
+ // add lq trackid to local map
+ self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
+
+ self.localStream.addTrack(lqStream.getVideoTracks()[0]);
+ success(self.localStream);
+ }, err);
+ }, err);
+};
+
+/**
+ * Prepares the local description for public usage (i.e. to be signaled
+ * through Jingle to the focus).
+ *
+ * @param desc
+ * @returns {RTCSessionDescription}
+ */
+SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
+ var sb;
+
+ if (!this.simulcastUtils.isValidDescription(desc)) {
+ return desc;
+ }
+
+ sb = desc.sdp.split('\r\n');
+
+ this._groupLocalVideoSources(sb);
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+
+ this.logger.fine('Grouped local video sources');
+ this.logger.fine(desc.sdp);
+
+ return desc;
+};
+
+/**
+ * Ensures that the simulcast group is present in the answer, _if_ native
+ * simulcast is enabled,
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimpleSimulcastSender.prototype.transformAnswer = function (desc) {
+ return desc;
+};
+
+
+/**
+ *
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) {
+
+ var sb = desc.sdp.split('\r\n');
+
+ this.simulcastUtils._removeSimulcastGroup(sb);
+
+ desc = new RTCSessionDescription({
+ type: desc.type,
+ sdp: sb.join('\r\n')
+ });
+
+ this.logger.fine('Transformed local description');
+ this.logger.fine(desc.sdp);
+
+ return desc;
+};
+
+SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
+ var trackid;
+
+ var self = this;
+ this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
+ if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) {
+ // Search for the track id that corresponds to the ssrc
+ if (self._localMaps.msid2ssrc[tid] == ssrc) {
+ trackid = tid;
+ return true;
+ }
+ }) && self.localStream.getVideoTracks().some(function (track) {
+ // Start/stop the track that corresponds to the track id
+ if (track.id === trackid) {
+ track.enabled = enabled;
+ return true;
+ }
+ })) {
+ this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
+ $(document).trigger(enabled
+ ? 'simulcastlayerstarted'
+ : 'simulcastlayerstopped');
+ } else {
+ this.logger.error("I don't have a local stream with SSRC " + ssrc);
+ }
+};
+
+SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender;
+
+function NoSimulcastSender() {
+ SimulcastSender.call(this);
+}
+
+NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
+
+/**
+ * GUM for simulcast.
+ *
+ * @param constraints
+ * @param success
+ * @param err
+ */
+NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
+ navigator.webkitGetUserMedia(constraints, function (hqStream) {
+ success(hqStream);
+ }, err);
+};
+
+/**
+ * Prepares the local description for public usage (i.e. to be signaled
+ * through Jingle to the focus).
+ *
+ * @param desc
+ * @returns {RTCSessionDescription}
+ */
+NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
+ return desc;
+};
+
+/**
+ * Ensures that the simulcast group is present in the answer, _if_ native
+ * simulcast is enabled,
+ *
+ * @param desc
+ * @returns {*}
+ */
+NoSimulcastSender.prototype.transformAnswer = function (desc) {
+ return desc;
+};
+
+
+/**
+ *
+ *
+ * @param desc
+ * @returns {*}
+ */
+NoSimulcastSender.prototype.transformLocalDescription = function (desc) {
+ return desc;
+};
+
+NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
+
+};
+
+NoSimulcastSender.prototype.constructor = NoSimulcastSender;
+
+module.exports = {
+ "native": NativeSimulcastSender,
+ "no": NoSimulcastSender
+}
+
+},{"./SimulcastLogger":40,"./SimulcastUtils":43}],43:[function(require,module,exports){
+var SimulcastLogger = require("./SimulcastLogger");
+
+/**
+ *
+ * @constructor
+ */
+function SimulcastUtils() {
+ this.logger = new SimulcastLogger("SimulcastUtils", 1);
+}
+
+/**
+ *
+ * @type {{}}
+ * @private
+ */
+SimulcastUtils.prototype._emptyCompoundIndex = {};
+
+/**
+ *
+ * @param lines
+ * @param videoSources
+ * @private
+ */
+SimulcastUtils.prototype._replaceVideoSources = function (lines, videoSources) {
+ var i, inVideo = false, index = -1, howMany = 0;
+
+ this.logger.info('Replacing video sources...');
+
+ for (i = 0; i < lines.length; i++) {
+ if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
+ // Out of video.
+ break;
+ }
+
+ if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
+ // In video.
+ inVideo = true;
+ }
+
+ if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
+ || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
+
+ if (index === -1) {
+ index = i;
+ }
+
+ howMany++;
+ }
+ }
+
+ // efficiency baby ;)
+ lines.splice.apply(lines,
+ [index, howMany].concat(videoSources));
+
+};
+
+SimulcastUtils.prototype.isValidDescription = function (desc)
+{
+ return desc && desc != null
+ && desc.type && desc.type != ''
+ && desc.sdp && desc.sdp != '';
+};
+
+SimulcastUtils.prototype._getVideoSources = function (lines) {
+ var i, inVideo = false, sb = [];
+
+ this.logger.info('Getting video sources...');
+
+ for (i = 0; i < lines.length; i++) {
+ if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
+ // Out of video.
+ break;
+ }
+
+ if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
+ // In video.
+ inVideo = true;
+ }
+
+ if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
+ // In SSRC.
+ sb.push(lines[i]);
+ }
+
+ if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
+ sb.push(lines[i]);
+ }
+ }
+
+ return sb;
+};
+
+SimulcastUtils.prototype.parseMedia = function (lines, mediatypes) {
+ var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
+ ssrc_attribute, group, semantics, skip = true;
+
+ this.logger.info('Parsing media sources...');
+
+ for (i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, 'm='.length) === 'm=') {
+
+ type = lines[i]
+ .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
+ skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
+
+ if (!skip) {
+ cur_media = {
+ 'type': type,
+ 'sources': {},
+ 'groups': []
+ };
+
+ res.push(cur_media);
+ }
+
+ } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
+
+ idx = lines[i].indexOf(' ');
+ ssrc = lines[i].substring('a=ssrc:'.length, idx);
+ if (cur_media.sources[ssrc] === undefined) {
+ cur_ssrc = {'ssrc': ssrc};
+ cur_media.sources[ssrc] = cur_ssrc;
+ }
+
+ ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
+ cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
+
+ if (cur_media.base === undefined) {
+ cur_media.base = cur_ssrc;
+ }
+
+ } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
+ idx = lines[i].indexOf(' ');
+ semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
+ ssrcs = lines[i].substr(idx).trim().split(' ');
+ group = {
+ 'semantics': semantics,
+ 'ssrcs': ssrcs
+ };
+ cur_media.groups.push(group);
+ } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
+ lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
+ lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
+ lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
+
+ cur_media.direction = lines[i].substring('a='.length);
+ }
+ }
+
+ return res;
+};
+
+/**
+ * The _indexOfArray() method returns the first a CompoundIndex at which a
+ * given element can be found in the array, or _emptyCompoundIndex if it is
+ * not present.
+ *
+ * Example:
+ *
+ * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ])
+ *
+ * returns {row: 2, column: 14}
+ *
+ * @param needle
+ * @param haystack
+ * @param start
+ * @returns {}
+ * @private
+ */
+SimulcastUtils.prototype._indexOfArray = function (needle, haystack, start) {
+ var length = haystack.length, idx, i;
+
+ if (!start) {
+ start = 0;
+ }
+
+ for (i = start; i < length; i++) {
+ idx = haystack[i].indexOf(needle);
+ if (idx !== -1) {
+ return {row: i, column: idx};
+ }
+ }
+ return this._emptyCompoundIndex;
+};
+
+SimulcastUtils.prototype._removeSimulcastGroup = function (lines) {
+ var i;
+
+ for (i = lines.length - 1; i >= 0; i--) {
+ if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
+ lines.splice(i, 1);
+ }
+ }
+};
+
+SimulcastUtils.prototype._compileVideoSources = function (videoSources) {
+ var sb = [], ssrc, addedSSRCs = [];
+
+ this.logger.info('Compiling video sources...');
+
+ // Add the groups
+ if (videoSources.groups && videoSources.groups.length !== 0) {
+ videoSources.groups.forEach(function (group) {
+ if (group.ssrcs && group.ssrcs.length !== 0) {
+ sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
+
+ // if (group.semantics !== 'SIM') {
+ group.ssrcs.forEach(function (ssrc) {
+ addedSSRCs.push(ssrc);
+ sb.splice.apply(sb, [sb.length, 0].concat([
+ ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
+ ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
+ });
+ //}
+ }
+ });
+ }
+
+ // Then add any free sources.
+ if (videoSources.sources) {
+ for (ssrc in videoSources.sources) {
+ if (addedSSRCs.indexOf(ssrc) === -1) {
+ sb.splice.apply(sb, [sb.length, 0].concat([
+ ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
+ ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
+ }
+ }
+ }
+
+ return sb;
+};
+
module.exports = SimulcastUtils;
-},{"./SimulcastLogger":39}],43:[function(require,module,exports){
-/*jslint plusplus: true */
-/*jslint nomen: true*/
-
-var SimulcastSender = require("./SimulcastSender");
-var NoSimulcastSender = SimulcastSender["no"];
-var NativeSimulcastSender = SimulcastSender["native"];
-var SimulcastReceiver = require("./SimulcastReceiver");
-var SimulcastUtils = require("./SimulcastUtils");
-var RTCEvents = require("../../service/RTC/RTCEvents");
-
-
-/**
- *
- * @constructor
- */
-function SimulcastManager() {
-
- // Create the simulcast utilities.
- this.simulcastUtils = new SimulcastUtils();
-
- // Create remote simulcast.
- this.simulcastReceiver = new SimulcastReceiver();
-
- // Initialize local simulcast.
-
- // TODO(gp) move into SimulcastManager.prototype.getUserMedia and take into
- // account constraints.
- if (!config.enableSimulcast) {
- this.simulcastSender = new NoSimulcastSender();
- } else {
-
- var isChromium = window.chrome,
- vendorName = window.navigator.vendor;
- if(isChromium !== null && isChromium !== undefined
- /* skip opera */
- && vendorName === "Google Inc."
- /* skip Chromium as suggested by fippo */
- && !window.navigator.appVersion.match(/Chromium\//) ) {
- var ver = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10);
- if (ver > 37) {
- this.simulcastSender = new NativeSimulcastSender();
- } else {
- this.simulcastSender = new NoSimulcastSender();
- }
- } else {
- this.simulcastSender = new NoSimulcastSender();
- }
-
- }
- APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGED,
- function (endpointSimulcastLayers) {
- endpointSimulcastLayers.forEach(function (esl) {
- var ssrc = esl.simulcastLayer.primarySSRC;
- simulcast._setReceivingVideoStream(esl.endpoint, ssrc);
- });
- });
- APP.RTC.addListener(RTCEvents.SIMULCAST_START, function (simulcastLayer) {
- var ssrc = simulcastLayer.primarySSRC;
- simulcast._setLocalVideoStreamEnabled(ssrc, true);
- });
- APP.RTC.addListener(RTCEvents.SIMULCAST_STOP, function (simulcastLayer) {
- var ssrc = simulcastLayer.primarySSRC;
- simulcast._setLocalVideoStreamEnabled(ssrc, false);
- });
-
-}
-
-/**
- * Restores the simulcast groups of the remote description. In
- * transformRemoteDescription we remove those in order for the set remote
- * description to succeed. The focus needs the signal the groups to new
- * participants.
- *
- * @param desc
- * @returns {*}
- */
-SimulcastManager.prototype.reverseTransformRemoteDescription = function (desc) {
- return this.simulcastReceiver.reverseTransformRemoteDescription(desc);
-};
-
-/**
- * Removes the ssrc-group:SIM from the remote description bacause Chrome
- * either gets confused and thinks this is an FID group or, if an FID group
- * is already present, it fails to set the remote description.
- *
- * @param desc
- * @returns {*}
- */
-SimulcastManager.prototype.transformRemoteDescription = function (desc) {
- return this.simulcastReceiver.transformRemoteDescription(desc);
-};
-
-/**
- * Gets the fully qualified msid (stream.id + track.id) associated to the
- * SSRC.
- *
- * @param ssrc
- * @returns {*}
- */
-SimulcastManager.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
- return this.simulcastReceiver.getRemoteVideoStreamIdBySSRC(ssrc);
-};
-
-/**
- * Returns a stream with single video track, the one currently being
- * received by this endpoint.
- *
- * @param stream the remote simulcast stream.
- * @returns {webkitMediaStream}
- */
-SimulcastManager.prototype.getReceivingVideoStream = function (stream) {
- return this.simulcastReceiver.getReceivingVideoStream(stream);
-};
-
-/**
- *
- *
- * @param desc
- * @returns {*}
- */
-SimulcastManager.prototype.transformLocalDescription = function (desc) {
- return this.simulcastSender.transformLocalDescription(desc);
-};
-
-/**
- *
- * @returns {*}
- */
-SimulcastManager.prototype.getLocalVideoStream = function() {
- return this.simulcastSender.getLocalVideoStream();
-};
-
-/**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
-SimulcastManager.prototype.getUserMedia = function (constraints, success, err) {
-
- this.simulcastSender.getUserMedia(constraints, success, err);
-};
-
-/**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
-SimulcastManager.prototype.reverseTransformLocalDescription = function (desc) {
- return this.simulcastSender.reverseTransformLocalDescription(desc);
-};
-
-/**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
-SimulcastManager.prototype.transformAnswer = function (desc) {
- return this.simulcastSender.transformAnswer(desc);
-};
-
-SimulcastManager.prototype.getReceivingSSRC = function (jid) {
- return this.simulcastReceiver.getReceivingSSRC(jid);
-};
-
-SimulcastManager.prototype.getReceivingVideoStreamBySSRC = function (msid) {
- return this.simulcastReceiver.getReceivingVideoStreamBySSRC(msid);
-};
-
-/**
- *
- * @param lines
- * @param mediatypes
- * @returns {*}
- */
-SimulcastManager.prototype.parseMedia = function(lines, mediatypes) {
- var sb = lines.sdp.split('\r\n');
- return this.simulcastUtils.parseMedia(sb, mediatypes);
-};
-
-SimulcastManager.prototype._setReceivingVideoStream = function(resource, ssrc) {
- this.simulcastReceiver._setReceivingVideoStream(resource, ssrc);
-};
-
-SimulcastManager.prototype._setLocalVideoStreamEnabled = function(ssrc, enabled) {
- this.simulcastSender._setLocalVideoStreamEnabled(ssrc, enabled);
-};
-
-SimulcastManager.prototype.resetSender = function() {
- if (typeof this.simulcastSender.reset === 'function'){
- this.simulcastSender.reset();
- }
-};
-
-var simulcast = new SimulcastManager();
-
+},{"./SimulcastLogger":40}],44:[function(require,module,exports){
+/*jslint plusplus: true */
+/*jslint nomen: true*/
+
+var SimulcastSender = require("./SimulcastSender");
+var NoSimulcastSender = SimulcastSender["no"];
+var NativeSimulcastSender = SimulcastSender["native"];
+var SimulcastReceiver = require("./SimulcastReceiver");
+var SimulcastUtils = require("./SimulcastUtils");
+var RTCEvents = require("../../service/RTC/RTCEvents");
+
+
+/**
+ *
+ * @constructor
+ */
+function SimulcastManager() {
+
+ // Create the simulcast utilities.
+ this.simulcastUtils = new SimulcastUtils();
+
+ // Create remote simulcast.
+ this.simulcastReceiver = new SimulcastReceiver();
+
+ // Initialize local simulcast.
+
+ // TODO(gp) move into SimulcastManager.prototype.getUserMedia and take into
+ // account constraints.
+ if (!config.enableSimulcast) {
+ this.simulcastSender = new NoSimulcastSender();
+ } else {
+
+ var isChromium = window.chrome,
+ vendorName = window.navigator.vendor;
+ if(isChromium !== null && isChromium !== undefined
+ /* skip opera */
+ && vendorName === "Google Inc."
+ /* skip Chromium as suggested by fippo */
+ && !window.navigator.appVersion.match(/Chromium\//) ) {
+ var ver = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10);
+ if (ver > 37) {
+ this.simulcastSender = new NativeSimulcastSender();
+ } else {
+ this.simulcastSender = new NoSimulcastSender();
+ }
+ } else {
+ this.simulcastSender = new NoSimulcastSender();
+ }
+
+ }
+ APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGED,
+ function (endpointSimulcastLayers) {
+ endpointSimulcastLayers.forEach(function (esl) {
+ var ssrc = esl.simulcastLayer.primarySSRC;
+ simulcast._setReceivingVideoStream(esl.endpoint, ssrc);
+ });
+ });
+ APP.RTC.addListener(RTCEvents.SIMULCAST_START, function (simulcastLayer) {
+ var ssrc = simulcastLayer.primarySSRC;
+ simulcast._setLocalVideoStreamEnabled(ssrc, true);
+ });
+ APP.RTC.addListener(RTCEvents.SIMULCAST_STOP, function (simulcastLayer) {
+ var ssrc = simulcastLayer.primarySSRC;
+ simulcast._setLocalVideoStreamEnabled(ssrc, false);
+ });
+
+}
+
+/**
+ * Restores the simulcast groups of the remote description. In
+ * transformRemoteDescription we remove those in order for the set remote
+ * description to succeed. The focus needs the signal the groups to new
+ * participants.
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastManager.prototype.reverseTransformRemoteDescription = function (desc) {
+ return this.simulcastReceiver.reverseTransformRemoteDescription(desc);
+};
+
+/**
+ * Removes the ssrc-group:SIM from the remote description bacause Chrome
+ * either gets confused and thinks this is an FID group or, if an FID group
+ * is already present, it fails to set the remote description.
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastManager.prototype.transformRemoteDescription = function (desc) {
+ return this.simulcastReceiver.transformRemoteDescription(desc);
+};
+
+/**
+ * Gets the fully qualified msid (stream.id + track.id) associated to the
+ * SSRC.
+ *
+ * @param ssrc
+ * @returns {*}
+ */
+SimulcastManager.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
+ return this.simulcastReceiver.getRemoteVideoStreamIdBySSRC(ssrc);
+};
+
+/**
+ * Returns a stream with single video track, the one currently being
+ * received by this endpoint.
+ *
+ * @param stream the remote simulcast stream.
+ * @returns {webkitMediaStream}
+ */
+SimulcastManager.prototype.getReceivingVideoStream = function (stream) {
+ return this.simulcastReceiver.getReceivingVideoStream(stream);
+};
+
+/**
+ *
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastManager.prototype.transformLocalDescription = function (desc) {
+ return this.simulcastSender.transformLocalDescription(desc);
+};
+
+/**
+ *
+ * @returns {*}
+ */
+SimulcastManager.prototype.getLocalVideoStream = function() {
+ return this.simulcastSender.getLocalVideoStream();
+};
+
+/**
+ * GUM for simulcast.
+ *
+ * @param constraints
+ * @param success
+ * @param err
+ */
+SimulcastManager.prototype.getUserMedia = function (constraints, success, err) {
+
+ this.simulcastSender.getUserMedia(constraints, success, err);
+};
+
+/**
+ * Prepares the local description for public usage (i.e. to be signaled
+ * through Jingle to the focus).
+ *
+ * @param desc
+ * @returns {RTCSessionDescription}
+ */
+SimulcastManager.prototype.reverseTransformLocalDescription = function (desc) {
+ return this.simulcastSender.reverseTransformLocalDescription(desc);
+};
+
+/**
+ * Ensures that the simulcast group is present in the answer, _if_ native
+ * simulcast is enabled,
+ *
+ * @param desc
+ * @returns {*}
+ */
+SimulcastManager.prototype.transformAnswer = function (desc) {
+ return this.simulcastSender.transformAnswer(desc);
+};
+
+SimulcastManager.prototype.getReceivingSSRC = function (jid) {
+ return this.simulcastReceiver.getReceivingSSRC(jid);
+};
+
+SimulcastManager.prototype.getReceivingVideoStreamBySSRC = function (msid) {
+ return this.simulcastReceiver.getReceivingVideoStreamBySSRC(msid);
+};
+
+/**
+ *
+ * @param lines
+ * @param mediatypes
+ * @returns {*}
+ */
+SimulcastManager.prototype.parseMedia = function(lines, mediatypes) {
+ var sb = lines.sdp.split('\r\n');
+ return this.simulcastUtils.parseMedia(sb, mediatypes);
+};
+
+SimulcastManager.prototype._setReceivingVideoStream = function(resource, ssrc) {
+ this.simulcastReceiver._setReceivingVideoStream(resource, ssrc);
+};
+
+SimulcastManager.prototype._setLocalVideoStreamEnabled = function(ssrc, enabled) {
+ this.simulcastSender._setLocalVideoStreamEnabled(ssrc, enabled);
+};
+
+SimulcastManager.prototype.resetSender = function() {
+ if (typeof this.simulcastSender.reset === 'function'){
+ this.simulcastSender.reset();
+ }
+};
+
+var simulcast = new SimulcastManager();
+
module.exports = simulcast;
-},{"../../service/RTC/RTCEvents":89,"./SimulcastReceiver":40,"./SimulcastSender":41,"./SimulcastUtils":42}],44:[function(require,module,exports){
-/**
- * Provides statistics for the local stream.
- */
-
-
-/**
- * Size of the webaudio analizer buffer.
- * @type {number}
- */
-var WEBAUDIO_ANALIZER_FFT_SIZE = 2048;
-
-/**
- * Value of the webaudio analizer smoothing time parameter.
- * @type {number}
- */
-var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8;
-
-/**
- * Converts time domain data array to audio level.
- * @param array the time domain data array.
- * @returns {number} the audio level
- */
-function timeDomainDataToAudioLevel(samples) {
-
- var maxVolume = 0;
-
- var length = samples.length;
-
- for (var i = 0; i < length; i++) {
- if (maxVolume < samples[i])
- maxVolume = samples[i];
- }
-
- return parseFloat(((maxVolume - 127) / 128).toFixed(3));
-};
-
-/**
- * Animates audio level change
- * @param newLevel the new audio level
- * @param lastLevel the last audio level
- * @returns {Number} the audio level to be set
- */
-function animateLevel(newLevel, lastLevel)
-{
- var value = 0;
- var diff = lastLevel - newLevel;
- if(diff > 0.2)
- {
- value = lastLevel - 0.2;
- }
- else if(diff < -0.4)
- {
- value = lastLevel + 0.4;
- }
- else
- {
- value = newLevel;
- }
-
- return parseFloat(value.toFixed(3));
-}
-
-
-/**
- * LocalStatsCollector calculates statistics for the local stream.
- *
- * @param stream the local stream
- * @param interval stats refresh interval given in ms.
- * @param {function(LocalStatsCollector)} updateCallback the callback called on stats
- * update.
- * @constructor
- */
-function LocalStatsCollector(stream, interval, statisticsService, eventEmitter) {
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- this.stream = stream;
- this.intervalId = null;
- this.intervalMilis = interval;
- this.eventEmitter = eventEmitter;
- this.audioLevel = 0;
- this.statisticsService = statisticsService;
-}
-
-/**
- * Starts the collecting the statistics.
- */
-LocalStatsCollector.prototype.start = function () {
- if (config.disableAudioLevels || !window.AudioContext)
- return;
-
- var context = new AudioContext();
- var analyser = context.createAnalyser();
- analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
- analyser.fftSize = WEBAUDIO_ANALIZER_FFT_SIZE;
-
-
- var source = context.createMediaStreamSource(this.stream);
- source.connect(analyser);
-
-
- var self = this;
-
- this.intervalId = setInterval(
- function () {
- var array = new Uint8Array(analyser.frequencyBinCount);
- analyser.getByteTimeDomainData(array);
- var audioLevel = timeDomainDataToAudioLevel(array);
- if(audioLevel != self.audioLevel) {
- self.audioLevel = animateLevel(audioLevel, self.audioLevel);
- self.eventEmitter.emit(
- "statistics.audioLevel",
- self.statisticsService.LOCAL_JID,
- self.audioLevel);
- }
- },
- this.intervalMilis
- );
-
-};
-
-/**
- * Stops collecting the statistics.
- */
-LocalStatsCollector.prototype.stop = function () {
- if (this.intervalId) {
- clearInterval(this.intervalId);
- this.intervalId = null;
- }
-};
-
+},{"../../service/RTC/RTCEvents":90,"./SimulcastReceiver":41,"./SimulcastSender":42,"./SimulcastUtils":43}],45:[function(require,module,exports){
+/**
+ * Provides statistics for the local stream.
+ */
+
+
+/**
+ * Size of the webaudio analizer buffer.
+ * @type {number}
+ */
+var WEBAUDIO_ANALIZER_FFT_SIZE = 2048;
+
+/**
+ * Value of the webaudio analizer smoothing time parameter.
+ * @type {number}
+ */
+var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8;
+
+/**
+ * Converts time domain data array to audio level.
+ * @param array the time domain data array.
+ * @returns {number} the audio level
+ */
+function timeDomainDataToAudioLevel(samples) {
+
+ var maxVolume = 0;
+
+ var length = samples.length;
+
+ for (var i = 0; i < length; i++) {
+ if (maxVolume < samples[i])
+ maxVolume = samples[i];
+ }
+
+ return parseFloat(((maxVolume - 127) / 128).toFixed(3));
+};
+
+/**
+ * Animates audio level change
+ * @param newLevel the new audio level
+ * @param lastLevel the last audio level
+ * @returns {Number} the audio level to be set
+ */
+function animateLevel(newLevel, lastLevel)
+{
+ var value = 0;
+ var diff = lastLevel - newLevel;
+ if(diff > 0.2)
+ {
+ value = lastLevel - 0.2;
+ }
+ else if(diff < -0.4)
+ {
+ value = lastLevel + 0.4;
+ }
+ else
+ {
+ value = newLevel;
+ }
+
+ return parseFloat(value.toFixed(3));
+}
+
+
+/**
+ * LocalStatsCollector calculates statistics for the local stream.
+ *
+ * @param stream the local stream
+ * @param interval stats refresh interval given in ms.
+ * @param {function(LocalStatsCollector)} updateCallback the callback called on stats
+ * update.
+ * @constructor
+ */
+function LocalStatsCollector(stream, interval, statisticsService, eventEmitter) {
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ this.stream = stream;
+ this.intervalId = null;
+ this.intervalMilis = interval;
+ this.eventEmitter = eventEmitter;
+ this.audioLevel = 0;
+ this.statisticsService = statisticsService;
+}
+
+/**
+ * Starts the collecting the statistics.
+ */
+LocalStatsCollector.prototype.start = function () {
+ if (config.disableAudioLevels || !window.AudioContext)
+ return;
+
+ var context = new AudioContext();
+ var analyser = context.createAnalyser();
+ analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
+ analyser.fftSize = WEBAUDIO_ANALIZER_FFT_SIZE;
+
+
+ var source = context.createMediaStreamSource(this.stream);
+ source.connect(analyser);
+
+
+ var self = this;
+
+ this.intervalId = setInterval(
+ function () {
+ var array = new Uint8Array(analyser.frequencyBinCount);
+ analyser.getByteTimeDomainData(array);
+ var audioLevel = timeDomainDataToAudioLevel(array);
+ if(audioLevel != self.audioLevel) {
+ self.audioLevel = animateLevel(audioLevel, self.audioLevel);
+ self.eventEmitter.emit(
+ "statistics.audioLevel",
+ self.statisticsService.LOCAL_JID,
+ self.audioLevel);
+ }
+ },
+ this.intervalMilis
+ );
+
+};
+
+/**
+ * Stops collecting the statistics.
+ */
+LocalStatsCollector.prototype.stop = function () {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+};
+
module.exports = LocalStatsCollector;
-},{}],45:[function(require,module,exports){
-/* global ssrc2jid */
-/* jshint -W117 */
-var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
-
-
-/**
- * Calculates packet lost percent using the number of lost packets and the
- * number of all packet.
- * @param lostPackets the number of lost packets
- * @param totalPackets the number of all packets.
- * @returns {number} packet loss percent
- */
-function calculatePacketLoss(lostPackets, totalPackets) {
- if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0)
- return 0;
- return Math.round((lostPackets/totalPackets)*100);
-}
-
-function getStatValue(item, name) {
- if(!keyMap[APP.RTC.getBrowserType()][name])
- throw "The property isn't supported!";
- var key = keyMap[APP.RTC.getBrowserType()][name];
- return APP.RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_CHROME? item.stat(key) : item[key];
-}
-
-/**
- * Peer statistics data holder.
- * @constructor
- */
-function PeerStats()
-{
- this.ssrc2Loss = {};
- this.ssrc2AudioLevel = {};
- this.ssrc2bitrate = {};
- this.ssrc2resolution = {};
-}
-
-/**
- * The bandwidth
- * @type {{}}
- */
-PeerStats.bandwidth = {};
-
-/**
- * The bit rate
- * @type {{}}
- */
-PeerStats.bitrate = {};
-
-
-
-/**
- * The packet loss rate
- * @type {{}}
- */
-PeerStats.packetLoss = null;
-
-/**
- * Sets packets loss rate for given ssrc that blong to the peer
- * represented by this instance.
- * @param ssrc audio or video RTP stream SSRC.
- * @param lossRate new packet loss rate value to be set.
- */
-PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
-{
- this.ssrc2Loss[ssrc] = lossRate;
-};
-
-/**
- * Sets resolution for given ssrc that belong to the peer
- * represented by this instance.
- * @param ssrc audio or video RTP stream SSRC.
- * @param resolution new resolution value to be set.
- */
-PeerStats.prototype.setSsrcResolution = function (ssrc, resolution)
-{
- if(resolution === null && this.ssrc2resolution[ssrc])
- {
- delete this.ssrc2resolution[ssrc];
- }
- else if(resolution !== null)
- this.ssrc2resolution[ssrc] = resolution;
-};
-
-/**
- * Sets the bit rate for given ssrc that blong to the peer
- * represented by this instance.
- * @param ssrc audio or video RTP stream SSRC.
- * @param bitrate new bitrate value to be set.
- */
-PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate)
-{
- if(this.ssrc2bitrate[ssrc])
- {
- this.ssrc2bitrate[ssrc].download += bitrate.download;
- this.ssrc2bitrate[ssrc].upload += bitrate.upload;
- }
- else {
- this.ssrc2bitrate[ssrc] = bitrate;
- }
-};
-
-/**
- * Sets new audio level(input or output) for given ssrc that identifies
- * the stream which belongs to the peer represented by this instance.
- * @param ssrc RTP stream SSRC for which current audio level value will be
- * updated.
- * @param audioLevel the new audio level value to be set. Value is truncated to
- * fit the range from 0 to 1.
- */
-PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
-{
- // Range limit 0 - 1
- this.ssrc2AudioLevel[ssrc] = formatAudioLevel(audioLevel);
-};
-
-function formatAudioLevel(audioLevel) {
- return Math.min(Math.max(audioLevel, 0), 1);
-}
-
-/**
- * Array with the transport information.
- * @type {Array}
- */
-PeerStats.transport = [];
-
-
-/**
- * StatsCollector registers for stats updates of given
- * peerconnection in given interval . On each update particular
- * stats are extracted and put in {@link PeerStats} objects. Once the processing
- * is done audioLevelsUpdateCallback is called with this
- * instance as an event source.
- *
- * @param peerconnection webRTC peer connection object.
- * @param interval stats refresh interval given in ms.
- * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
- * called on stats update.
- * @constructor
- */
-function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter)
-{
- this.peerconnection = peerconnection;
- this.baselineAudioLevelsReport = null;
- this.currentAudioLevelsReport = null;
- this.currentStatsReport = null;
- this.baselineStatsReport = null;
- this.audioLevelsIntervalId = null;
- this.eventEmitter = eventEmitter;
-
- /**
- * Gather PeerConnection stats once every this many milliseconds.
- */
- this.GATHER_INTERVAL = 15000;
-
- /**
- * Log stats via the focus once every this many milliseconds.
- */
- this.LOG_INTERVAL = 60000;
-
- /**
- * Gather stats and store them in this.statsToBeLogged.
- */
- this.gatherStatsIntervalId = null;
-
- /**
- * Send the stats already saved in this.statsToBeLogged to be logged via
- * the focus.
- */
- this.logStatsIntervalId = null;
-
- /**
- * Stores the statistics which will be send to the focus to be logged.
- */
- this.statsToBeLogged =
- {
- timestamps: [],
- stats: {}
- };
-
- // Updates stats interval
- this.audioLevelsIntervalMilis = audioLevelsInterval;
-
- this.statsIntervalId = null;
- this.statsIntervalMilis = statsInterval;
- // Map of jids to PeerStats
- this.jid2stats = {};
-}
-
-module.exports = StatsCollector;
-
-/**
- * Stops stats updates.
- */
-StatsCollector.prototype.stop = function () {
- if (this.audioLevelsIntervalId) {
- clearInterval(this.audioLevelsIntervalId);
- this.audioLevelsIntervalId = null;
- }
-
- if (this.statsIntervalId)
- {
- clearInterval(this.statsIntervalId);
- this.statsIntervalId = null;
- }
-
- if(this.logStatsIntervalId)
- {
- clearInterval(this.logStatsIntervalId);
- this.logStatsIntervalId = null;
- }
-
- if(this.gatherStatsIntervalId)
- {
- clearInterval(this.gatherStatsIntervalId);
- this.gatherStatsIntervalId = null;
- }
-};
-
-/**
- * Callback passed to getStats method.
- * @param error an error that occurred on getStats call.
- */
-StatsCollector.prototype.errorCallback = function (error)
-{
- console.error("Get stats error", error);
- this.stop();
-};
-
-/**
- * Starts stats updates.
- */
-StatsCollector.prototype.start = function ()
-{
- var self = this;
- if(!config.disableAudioLevels) {
- this.audioLevelsIntervalId = setInterval(
- function () {
- // Interval updates
- self.peerconnection.getStats(
- function (report) {
- var results = null;
- if (!report || !report.result ||
- typeof report.result != 'function') {
- results = report;
- }
- else {
- results = report.result();
- }
- //console.error("Got interval report", results);
- self.currentAudioLevelsReport = results;
- self.processAudioLevelReport();
- self.baselineAudioLevelsReport =
- self.currentAudioLevelsReport;
- },
- self.errorCallback
- );
- },
- self.audioLevelsIntervalMilis
- );
- }
-
- if(!config.disableStats) {
- this.statsIntervalId = setInterval(
- function () {
- // Interval updates
- self.peerconnection.getStats(
- function (report) {
- var results = null;
- if (!report || !report.result ||
- typeof report.result != 'function') {
- //firefox
- results = report;
- }
- else {
- //chrome
- results = report.result();
- }
- //console.error("Got interval report", results);
- self.currentStatsReport = results;
- try {
- self.processStatsReport();
- }
- catch (e) {
- console.error("Unsupported key:" + e, e);
- }
-
- self.baselineStatsReport = self.currentStatsReport;
- },
- self.errorCallback
- );
- },
- self.statsIntervalMilis
- );
- }
-
- if (config.logStats) {
- this.gatherStatsIntervalId = setInterval(
- function () {
- self.peerconnection.getStats(
- function (report) {
- self.addStatsToBeLogged(report.result());
- },
- function () {
- }
- );
- },
- this.GATHER_INTERVAL
- );
-
- this.logStatsIntervalId = setInterval(
- function() { self.logStats(); },
- this.LOG_INTERVAL);
- }
-};
-
-/**
- * Checks whether a certain record should be included in the logged statistics.
- */
-function acceptStat(reportId, reportType, statName) {
- if (reportType == "googCandidatePair" && statName == "googChannelId")
- return false;
-
- if (reportType == "ssrc") {
- if (statName == "googTrackId" ||
- statName == "transportId" ||
- statName == "ssrc")
- return false;
- }
-
- return true;
-}
-
-/**
- * Checks whether a certain record should be included in the logged statistics.
- */
-function acceptReport(id, type) {
- if (id.substring(0, 15) == "googCertificate" ||
- id.substring(0, 9) == "googTrack" ||
- id.substring(0, 20) == "googLibjingleSession")
- return false;
-
- if (type == "googComponent")
- return false;
-
- return true;
-}
-
-/**
- * Converts the stats to the format used for logging, and saves the data in
- * this.statsToBeLogged.
- * @param reports Reports as given by webkitRTCPerConnection.getStats.
- */
-StatsCollector.prototype.addStatsToBeLogged = function (reports) {
- var self = this;
- var num_records = this.statsToBeLogged.timestamps.length;
- this.statsToBeLogged.timestamps.push(new Date().getTime());
- reports.map(function (report) {
- if (!acceptReport(report.id, report.type))
- return;
- var stat = self.statsToBeLogged.stats[report.id];
- if (!stat) {
- stat = self.statsToBeLogged.stats[report.id] = {};
- }
- stat.type = report.type;
- report.names().map(function (name) {
- if (!acceptStat(report.id, report.type, name))
- return;
- var values = stat[name];
- if (!values) {
- values = stat[name] = [];
- }
- while (values.length < num_records) {
- values.push(null);
- }
- values.push(report.stat(name));
- });
- });
-};
-
-StatsCollector.prototype.logStats = function () {
-
- if(!APP.xmpp.sendLogs(this.statsToBeLogged))
- return;
- // Reset the stats
- this.statsToBeLogged.stats = {};
- this.statsToBeLogged.timestamps = [];
-};
-var keyMap = {};
-keyMap[RTCBrowserType.RTC_BROWSER_FIREFOX] = {
- "ssrc": "ssrc",
- "packetsReceived": "packetsReceived",
- "packetsLost": "packetsLost",
- "packetsSent": "packetsSent",
- "bytesReceived": "bytesReceived",
- "bytesSent": "bytesSent"
-};
-keyMap[RTCBrowserType.RTC_BROWSER_CHROME] = {
- "receiveBandwidth": "googAvailableReceiveBandwidth",
- "sendBandwidth": "googAvailableSendBandwidth",
- "remoteAddress": "googRemoteAddress",
- "transportType": "googTransportType",
- "localAddress": "googLocalAddress",
- "activeConnection": "googActiveConnection",
- "ssrc": "ssrc",
- "packetsReceived": "packetsReceived",
- "packetsSent": "packetsSent",
- "packetsLost": "packetsLost",
- "bytesReceived": "bytesReceived",
- "bytesSent": "bytesSent",
- "googFrameHeightReceived": "googFrameHeightReceived",
- "googFrameWidthReceived": "googFrameWidthReceived",
- "googFrameHeightSent": "googFrameHeightSent",
- "googFrameWidthSent": "googFrameWidthSent",
- "audioInputLevel": "audioInputLevel",
- "audioOutputLevel": "audioOutputLevel"
-};
-
-
-/**
- * Stats processing logic.
- */
-StatsCollector.prototype.processStatsReport = function () {
- if (!this.baselineStatsReport) {
- return;
- }
-
- for (var idx in this.currentStatsReport) {
- var now = this.currentStatsReport[idx];
- try {
- if (getStatValue(now, 'receiveBandwidth') ||
- getStatValue(now, 'sendBandwidth')) {
- PeerStats.bandwidth = {
- "download": Math.round(
- (getStatValue(now, 'receiveBandwidth')) / 1000),
- "upload": Math.round(
- (getStatValue(now, 'sendBandwidth')) / 1000)
- };
- }
- }
- catch(e){/*not supported*/}
-
- if(now.type == 'googCandidatePair')
- {
- var ip, type, localIP, active;
- try {
- ip = getStatValue(now, 'remoteAddress');
- type = getStatValue(now, "transportType");
- localIP = getStatValue(now, "localAddress");
- active = getStatValue(now, "activeConnection");
- }
- catch(e){/*not supported*/}
- if(!ip || !type || !localIP || active != "true")
- continue;
- var addressSaved = false;
- for(var i = 0; i < PeerStats.transport.length; i++)
- {
- if(PeerStats.transport[i].ip == ip &&
- PeerStats.transport[i].type == type &&
- PeerStats.transport[i].localip == localIP)
- {
- addressSaved = true;
- }
- }
- if(addressSaved)
- continue;
- PeerStats.transport.push({localip: localIP, ip: ip, type: type});
- continue;
- }
-
- if(now.type == "candidatepair")
- {
- if(now.state == "succeeded")
- continue;
-
- var local = this.currentStatsReport[now.localCandidateId];
- var remote = this.currentStatsReport[now.remoteCandidateId];
- PeerStats.transport.push({localip: local.ipAddress + ":" + local.portNumber,
- ip: remote.ipAddress + ":" + remote.portNumber, type: local.transport});
-
- }
-
- if (now.type != 'ssrc' && now.type != "outboundrtp" &&
- now.type != "inboundrtp") {
- continue;
- }
-
- var before = this.baselineStatsReport[idx];
- if (!before) {
- console.warn(getStatValue(now, 'ssrc') + ' not enough data');
- continue;
- }
-
- var ssrc = getStatValue(now, 'ssrc');
- if(!ssrc)
- continue;
- var jid = APP.xmpp.getJidFromSSRC(ssrc);
- if (!jid && (Date.now() - now.timestamp) < 3000) {
- console.warn("No jid for ssrc: " + ssrc);
- continue;
- }
-
- var jidStats = this.jid2stats[jid];
- if (!jidStats) {
- jidStats = new PeerStats();
- this.jid2stats[jid] = jidStats;
- }
-
-
- var isDownloadStream = true;
- var key = 'packetsReceived';
- if (!getStatValue(now, key))
- {
- isDownloadStream = false;
- key = 'packetsSent';
- if (!getStatValue(now, key))
- {
- console.warn("No packetsReceived nor packetSent stat found");
- continue;
- }
- }
- var packetsNow = getStatValue(now, key);
- if(!packetsNow || packetsNow < 0)
- packetsNow = 0;
-
- var packetsBefore = getStatValue(before, key);
- if(!packetsBefore || packetsBefore < 0)
- packetsBefore = 0;
- var packetRate = packetsNow - packetsBefore;
- if(!packetRate || packetRate < 0)
- packetRate = 0;
- var currentLoss = getStatValue(now, 'packetsLost');
- if(!currentLoss || currentLoss < 0)
- currentLoss = 0;
- var previousLoss = getStatValue(before, 'packetsLost');
- if(!previousLoss || previousLoss < 0)
- previousLoss = 0;
- var lossRate = currentLoss - previousLoss;
- if(!lossRate || lossRate < 0)
- lossRate = 0;
- var packetsTotal = (packetRate + lossRate);
-
- jidStats.setSsrcLoss(ssrc,
- {"packetsTotal": packetsTotal,
- "packetsLost": lossRate,
- "isDownloadStream": isDownloadStream});
-
-
- var bytesReceived = 0, bytesSent = 0;
- if(getStatValue(now, "bytesReceived"))
- {
- bytesReceived = getStatValue(now, "bytesReceived") -
- getStatValue(before, "bytesReceived");
- }
-
- if(getStatValue(now, "bytesSent"))
- {
- bytesSent = getStatValue(now, "bytesSent") -
- getStatValue(before, "bytesSent");
- }
-
- var time = Math.round((now.timestamp - before.timestamp) / 1000);
- if(bytesReceived <= 0 || time <= 0)
- {
- bytesReceived = 0;
- }
- else
- {
- bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
- }
-
- if(bytesSent <= 0 || time <= 0)
- {
- bytesSent = 0;
- }
- else
- {
- bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
- }
-
- jidStats.setSsrcBitrate(ssrc, {
- "download": bytesReceived,
- "upload": bytesSent});
-
- var resolution = {height: null, width: null};
- try {
- if (getStatValue(now, "googFrameHeightReceived") &&
- getStatValue(now, "googFrameWidthReceived")) {
- resolution.height = getStatValue(now, "googFrameHeightReceived");
- resolution.width = getStatValue(now, "googFrameWidthReceived");
- }
- else if (getStatValue(now, "googFrameHeightSent") &&
- getStatValue(now, "googFrameWidthSent")) {
- resolution.height = getStatValue(now, "googFrameHeightSent");
- resolution.width = getStatValue(now, "googFrameWidthSent");
- }
- }
- catch(e){/*not supported*/}
-
- if(resolution.height && resolution.width)
- {
- jidStats.setSsrcResolution(ssrc, resolution);
- }
- else
- {
- jidStats.setSsrcResolution(ssrc, null);
- }
-
-
- }
-
- var self = this;
- // Jid stats
- var totalPackets = {download: 0, upload: 0};
- var lostPackets = {download: 0, upload: 0};
- var bitrateDownload = 0;
- var bitrateUpload = 0;
- var resolutions = {};
- Object.keys(this.jid2stats).forEach(
- function (jid)
- {
- Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
- function (ssrc)
- {
- var type = "upload";
- if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream)
- type = "download";
- totalPackets[type] +=
- self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal;
- lostPackets[type] +=
- self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost;
- }
- );
- Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach(
- function (ssrc) {
- bitrateDownload +=
- self.jid2stats[jid].ssrc2bitrate[ssrc].download;
- bitrateUpload +=
- self.jid2stats[jid].ssrc2bitrate[ssrc].upload;
-
- delete self.jid2stats[jid].ssrc2bitrate[ssrc];
- }
- );
- resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
- }
- );
-
- PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload};
-
- PeerStats.packetLoss = {
- total:
- calculatePacketLoss(lostPackets.download + lostPackets.upload,
- totalPackets.download + totalPackets.upload),
- download:
- calculatePacketLoss(lostPackets.download, totalPackets.download),
- upload:
- calculatePacketLoss(lostPackets.upload, totalPackets.upload)
- };
- this.eventEmitter.emit("statistics.connectionstats",
- {
- "bitrate": PeerStats.bitrate,
- "packetLoss": PeerStats.packetLoss,
- "bandwidth": PeerStats.bandwidth,
- "resolution": resolutions,
- "transport": PeerStats.transport
- });
- PeerStats.transport = [];
-
-};
-
-/**
- * Stats processing logic.
- */
-StatsCollector.prototype.processAudioLevelReport = function ()
-{
- if (!this.baselineAudioLevelsReport)
- {
- return;
- }
-
- for (var idx in this.currentAudioLevelsReport)
- {
- var now = this.currentAudioLevelsReport[idx];
-
- if (now.type != 'ssrc')
- {
- continue;
- }
-
- var before = this.baselineAudioLevelsReport[idx];
- if (!before)
- {
- console.warn(getStatValue(now, 'ssrc') + ' not enough data');
- continue;
- }
-
- var ssrc = getStatValue(now, 'ssrc');
- var jid = APP.xmpp.getJidFromSSRC(ssrc);
- if (!jid && (Date.now() - now.timestamp) < 3000)
- {
- console.warn("No jid for ssrc: " + ssrc);
- continue;
- }
-
- var jidStats = this.jid2stats[jid];
- if (!jidStats)
- {
- jidStats = new PeerStats();
- this.jid2stats[jid] = jidStats;
- }
-
- // Audio level
- var audioLevel = null;
-
- try {
- audioLevel = getStatValue(now, 'audioInputLevel');
- if (!audioLevel)
- audioLevel = getStatValue(now, 'audioOutputLevel');
- }
- catch(e) {/*not supported*/
- console.warn("Audio Levels are not available in the statistics.");
- clearInterval(this.audioLevelsIntervalId);
- return;
- }
-
- if (audioLevel)
- {
- // TODO: can't find specs about what this value really is,
- // but it seems to vary between 0 and around 32k.
- audioLevel = audioLevel / 32767;
- jidStats.setSsrcAudioLevel(ssrc, audioLevel);
- if(jid != APP.xmpp.myJid())
- this.eventEmitter.emit("statistics.audioLevel", jid, audioLevel);
- }
-
- }
-
-
-};
-
-},{"../../service/RTC/RTCBrowserType":88}],46:[function(require,module,exports){
-/**
- * Created by hristo on 8/4/14.
- */
-var LocalStats = require("./LocalStatsCollector.js");
-var RTPStats = require("./RTPStatsCollector.js");
-var EventEmitter = require("events");
-var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-
-var eventEmitter = new EventEmitter();
-
-var localStats = null;
-
-var rtpStats = null;
-
-function stopLocal()
-{
- if(localStats)
- {
- localStats.stop();
- localStats = null;
- }
-}
-
-function stopRemote()
-{
- if(rtpStats)
- {
- rtpStats.stop();
- eventEmitter.emit("statistics.stop");
- rtpStats = null;
- }
-}
-
-function startRemoteStats (peerconnection) {
- if(rtpStats)
- {
- rtpStats.stop();
- rtpStats = null;
- }
-
- rtpStats = new RTPStats(peerconnection, 200, 2000, eventEmitter);
- rtpStats.start();
-}
-
-function onStreamCreated(stream)
-{
- if(stream.getOriginalStream().getAudioTracks().length === 0)
- return;
-
- localStats = new LocalStats(stream.getOriginalStream(), 200, statistics,
- eventEmitter);
- localStats.start();
-}
-
-function onDisposeConference(onUnload) {
- stopRemote();
- if(onUnload) {
- stopLocal();
- eventEmitter.removeAllListeners();
- }
-}
-
-
-var statistics =
-{
- /**
- * Indicates that this audio level is for local jid.
- * @type {string}
- */
- LOCAL_JID: 'local',
-
- addAudioLevelListener: function(listener)
- {
- eventEmitter.on("statistics.audioLevel", listener);
- },
-
- removeAudioLevelListener: function(listener)
- {
- eventEmitter.removeListener("statistics.audioLevel", listener);
- },
-
- addConnectionStatsListener: function(listener)
- {
- eventEmitter.on("statistics.connectionstats", listener);
- },
-
- removeConnectionStatsListener: function(listener)
- {
- eventEmitter.removeListener("statistics.connectionstats", listener);
- },
-
-
- addRemoteStatsStopListener: function(listener)
- {
- eventEmitter.on("statistics.stop", listener);
- },
-
- removeRemoteStatsStopListener: function(listener)
- {
- eventEmitter.removeListener("statistics.stop", listener);
- },
-
- stop: function () {
- stopLocal();
- stopRemote();
- if(eventEmitter)
- {
- eventEmitter.removeAllListeners();
- }
- },
-
- stopRemoteStatistics: function()
- {
- stopRemote();
- },
-
- start: function () {
- APP.RTC.addStreamListener(onStreamCreated,
- StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
- APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference);
- APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
- startRemoteStats(event.peerconnection);
- });
- }
-
-};
-
-
-
-
+},{}],46:[function(require,module,exports){
+/* global ssrc2jid */
+/* jshint -W117 */
+var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
+
+
+/**
+ * Calculates packet lost percent using the number of lost packets and the
+ * number of all packet.
+ * @param lostPackets the number of lost packets
+ * @param totalPackets the number of all packets.
+ * @returns {number} packet loss percent
+ */
+function calculatePacketLoss(lostPackets, totalPackets) {
+ if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0)
+ return 0;
+ return Math.round((lostPackets/totalPackets)*100);
+}
+
+function getStatValue(item, name) {
+ if(!keyMap[APP.RTC.getBrowserType()][name])
+ throw "The property isn't supported!";
+ var key = keyMap[APP.RTC.getBrowserType()][name];
+ return APP.RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_CHROME? item.stat(key) : item[key];
+}
+
+/**
+ * Peer statistics data holder.
+ * @constructor
+ */
+function PeerStats()
+{
+ this.ssrc2Loss = {};
+ this.ssrc2AudioLevel = {};
+ this.ssrc2bitrate = {};
+ this.ssrc2resolution = {};
+}
+
+/**
+ * The bandwidth
+ * @type {{}}
+ */
+PeerStats.bandwidth = {};
+
+/**
+ * The bit rate
+ * @type {{}}
+ */
+PeerStats.bitrate = {};
+
+
+
+/**
+ * The packet loss rate
+ * @type {{}}
+ */
+PeerStats.packetLoss = null;
+
+/**
+ * Sets packets loss rate for given ssrc that blong to the peer
+ * represented by this instance.
+ * @param ssrc audio or video RTP stream SSRC.
+ * @param lossRate new packet loss rate value to be set.
+ */
+PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
+{
+ this.ssrc2Loss[ssrc] = lossRate;
+};
+
+/**
+ * Sets resolution for given ssrc that belong to the peer
+ * represented by this instance.
+ * @param ssrc audio or video RTP stream SSRC.
+ * @param resolution new resolution value to be set.
+ */
+PeerStats.prototype.setSsrcResolution = function (ssrc, resolution)
+{
+ if(resolution === null && this.ssrc2resolution[ssrc])
+ {
+ delete this.ssrc2resolution[ssrc];
+ }
+ else if(resolution !== null)
+ this.ssrc2resolution[ssrc] = resolution;
+};
+
+/**
+ * Sets the bit rate for given ssrc that blong to the peer
+ * represented by this instance.
+ * @param ssrc audio or video RTP stream SSRC.
+ * @param bitrate new bitrate value to be set.
+ */
+PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate)
+{
+ if(this.ssrc2bitrate[ssrc])
+ {
+ this.ssrc2bitrate[ssrc].download += bitrate.download;
+ this.ssrc2bitrate[ssrc].upload += bitrate.upload;
+ }
+ else {
+ this.ssrc2bitrate[ssrc] = bitrate;
+ }
+};
+
+/**
+ * Sets new audio level(input or output) for given ssrc that identifies
+ * the stream which belongs to the peer represented by this instance.
+ * @param ssrc RTP stream SSRC for which current audio level value will be
+ * updated.
+ * @param audioLevel the new audio level value to be set. Value is truncated to
+ * fit the range from 0 to 1.
+ */
+PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
+{
+ // Range limit 0 - 1
+ this.ssrc2AudioLevel[ssrc] = formatAudioLevel(audioLevel);
+};
+
+function formatAudioLevel(audioLevel) {
+ return Math.min(Math.max(audioLevel, 0), 1);
+}
+
+/**
+ * Array with the transport information.
+ * @type {Array}
+ */
+PeerStats.transport = [];
+
+
+/**
+ * StatsCollector registers for stats updates of given
+ * peerconnection in given interval . On each update particular
+ * stats are extracted and put in {@link PeerStats} objects. Once the processing
+ * is done audioLevelsUpdateCallback is called with this
+ * instance as an event source.
+ *
+ * @param peerconnection webRTC peer connection object.
+ * @param interval stats refresh interval given in ms.
+ * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
+ * called on stats update.
+ * @constructor
+ */
+function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter)
+{
+ this.peerconnection = peerconnection;
+ this.baselineAudioLevelsReport = null;
+ this.currentAudioLevelsReport = null;
+ this.currentStatsReport = null;
+ this.baselineStatsReport = null;
+ this.audioLevelsIntervalId = null;
+ this.eventEmitter = eventEmitter;
+
+ /**
+ * Gather PeerConnection stats once every this many milliseconds.
+ */
+ this.GATHER_INTERVAL = 15000;
+
+ /**
+ * Log stats via the focus once every this many milliseconds.
+ */
+ this.LOG_INTERVAL = 60000;
+
+ /**
+ * Gather stats and store them in this.statsToBeLogged.
+ */
+ this.gatherStatsIntervalId = null;
+
+ /**
+ * Send the stats already saved in this.statsToBeLogged to be logged via
+ * the focus.
+ */
+ this.logStatsIntervalId = null;
+
+ /**
+ * Stores the statistics which will be send to the focus to be logged.
+ */
+ this.statsToBeLogged =
+ {
+ timestamps: [],
+ stats: {}
+ };
+
+ // Updates stats interval
+ this.audioLevelsIntervalMilis = audioLevelsInterval;
+
+ this.statsIntervalId = null;
+ this.statsIntervalMilis = statsInterval;
+ // Map of jids to PeerStats
+ this.jid2stats = {};
+}
+
+module.exports = StatsCollector;
+
+/**
+ * Stops stats updates.
+ */
+StatsCollector.prototype.stop = function () {
+ if (this.audioLevelsIntervalId) {
+ clearInterval(this.audioLevelsIntervalId);
+ this.audioLevelsIntervalId = null;
+ }
+
+ if (this.statsIntervalId)
+ {
+ clearInterval(this.statsIntervalId);
+ this.statsIntervalId = null;
+ }
+
+ if(this.logStatsIntervalId)
+ {
+ clearInterval(this.logStatsIntervalId);
+ this.logStatsIntervalId = null;
+ }
+
+ if(this.gatherStatsIntervalId)
+ {
+ clearInterval(this.gatherStatsIntervalId);
+ this.gatherStatsIntervalId = null;
+ }
+};
+
+/**
+ * Callback passed to getStats method.
+ * @param error an error that occurred on getStats call.
+ */
+StatsCollector.prototype.errorCallback = function (error)
+{
+ console.error("Get stats error", error);
+ this.stop();
+};
+
+/**
+ * Starts stats updates.
+ */
+StatsCollector.prototype.start = function ()
+{
+ var self = this;
+ if(!config.disableAudioLevels) {
+ this.audioLevelsIntervalId = setInterval(
+ function () {
+ // Interval updates
+ self.peerconnection.getStats(
+ function (report) {
+ var results = null;
+ if (!report || !report.result ||
+ typeof report.result != 'function') {
+ results = report;
+ }
+ else {
+ results = report.result();
+ }
+ //console.error("Got interval report", results);
+ self.currentAudioLevelsReport = results;
+ self.processAudioLevelReport();
+ self.baselineAudioLevelsReport =
+ self.currentAudioLevelsReport;
+ },
+ self.errorCallback
+ );
+ },
+ self.audioLevelsIntervalMilis
+ );
+ }
+
+ if(!config.disableStats) {
+ this.statsIntervalId = setInterval(
+ function () {
+ // Interval updates
+ self.peerconnection.getStats(
+ function (report) {
+ var results = null;
+ if (!report || !report.result ||
+ typeof report.result != 'function') {
+ //firefox
+ results = report;
+ }
+ else {
+ //chrome
+ results = report.result();
+ }
+ //console.error("Got interval report", results);
+ self.currentStatsReport = results;
+ try {
+ self.processStatsReport();
+ }
+ catch (e) {
+ console.error("Unsupported key:" + e, e);
+ }
+
+ self.baselineStatsReport = self.currentStatsReport;
+ },
+ self.errorCallback
+ );
+ },
+ self.statsIntervalMilis
+ );
+ }
+
+ if (config.logStats) {
+ this.gatherStatsIntervalId = setInterval(
+ function () {
+ self.peerconnection.getStats(
+ function (report) {
+ self.addStatsToBeLogged(report.result());
+ },
+ function () {
+ }
+ );
+ },
+ this.GATHER_INTERVAL
+ );
+
+ this.logStatsIntervalId = setInterval(
+ function() { self.logStats(); },
+ this.LOG_INTERVAL);
+ }
+};
+
+/**
+ * Checks whether a certain record should be included in the logged statistics.
+ */
+function acceptStat(reportId, reportType, statName) {
+ if (reportType == "googCandidatePair" && statName == "googChannelId")
+ return false;
+
+ if (reportType == "ssrc") {
+ if (statName == "googTrackId" ||
+ statName == "transportId" ||
+ statName == "ssrc")
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Checks whether a certain record should be included in the logged statistics.
+ */
+function acceptReport(id, type) {
+ if (id.substring(0, 15) == "googCertificate" ||
+ id.substring(0, 9) == "googTrack" ||
+ id.substring(0, 20) == "googLibjingleSession")
+ return false;
+
+ if (type == "googComponent")
+ return false;
+
+ return true;
+}
+
+/**
+ * Converts the stats to the format used for logging, and saves the data in
+ * this.statsToBeLogged.
+ * @param reports Reports as given by webkitRTCPerConnection.getStats.
+ */
+StatsCollector.prototype.addStatsToBeLogged = function (reports) {
+ var self = this;
+ var num_records = this.statsToBeLogged.timestamps.length;
+ this.statsToBeLogged.timestamps.push(new Date().getTime());
+ reports.map(function (report) {
+ if (!acceptReport(report.id, report.type))
+ return;
+ var stat = self.statsToBeLogged.stats[report.id];
+ if (!stat) {
+ stat = self.statsToBeLogged.stats[report.id] = {};
+ }
+ stat.type = report.type;
+ report.names().map(function (name) {
+ if (!acceptStat(report.id, report.type, name))
+ return;
+ var values = stat[name];
+ if (!values) {
+ values = stat[name] = [];
+ }
+ while (values.length < num_records) {
+ values.push(null);
+ }
+ values.push(report.stat(name));
+ });
+ });
+};
+
+StatsCollector.prototype.logStats = function () {
+
+ if(!APP.xmpp.sendLogs(this.statsToBeLogged))
+ return;
+ // Reset the stats
+ this.statsToBeLogged.stats = {};
+ this.statsToBeLogged.timestamps = [];
+};
+var keyMap = {};
+keyMap[RTCBrowserType.RTC_BROWSER_FIREFOX] = {
+ "ssrc": "ssrc",
+ "packetsReceived": "packetsReceived",
+ "packetsLost": "packetsLost",
+ "packetsSent": "packetsSent",
+ "bytesReceived": "bytesReceived",
+ "bytesSent": "bytesSent"
+};
+keyMap[RTCBrowserType.RTC_BROWSER_CHROME] = {
+ "receiveBandwidth": "googAvailableReceiveBandwidth",
+ "sendBandwidth": "googAvailableSendBandwidth",
+ "remoteAddress": "googRemoteAddress",
+ "transportType": "googTransportType",
+ "localAddress": "googLocalAddress",
+ "activeConnection": "googActiveConnection",
+ "ssrc": "ssrc",
+ "packetsReceived": "packetsReceived",
+ "packetsSent": "packetsSent",
+ "packetsLost": "packetsLost",
+ "bytesReceived": "bytesReceived",
+ "bytesSent": "bytesSent",
+ "googFrameHeightReceived": "googFrameHeightReceived",
+ "googFrameWidthReceived": "googFrameWidthReceived",
+ "googFrameHeightSent": "googFrameHeightSent",
+ "googFrameWidthSent": "googFrameWidthSent",
+ "audioInputLevel": "audioInputLevel",
+ "audioOutputLevel": "audioOutputLevel"
+};
+
+
+/**
+ * Stats processing logic.
+ */
+StatsCollector.prototype.processStatsReport = function () {
+ if (!this.baselineStatsReport) {
+ return;
+ }
+
+ for (var idx in this.currentStatsReport) {
+ var now = this.currentStatsReport[idx];
+ try {
+ if (getStatValue(now, 'receiveBandwidth') ||
+ getStatValue(now, 'sendBandwidth')) {
+ PeerStats.bandwidth = {
+ "download": Math.round(
+ (getStatValue(now, 'receiveBandwidth')) / 1000),
+ "upload": Math.round(
+ (getStatValue(now, 'sendBandwidth')) / 1000)
+ };
+ }
+ }
+ catch(e){/*not supported*/}
+
+ if(now.type == 'googCandidatePair')
+ {
+ var ip, type, localIP, active;
+ try {
+ ip = getStatValue(now, 'remoteAddress');
+ type = getStatValue(now, "transportType");
+ localIP = getStatValue(now, "localAddress");
+ active = getStatValue(now, "activeConnection");
+ }
+ catch(e){/*not supported*/}
+ if(!ip || !type || !localIP || active != "true")
+ continue;
+ var addressSaved = false;
+ for(var i = 0; i < PeerStats.transport.length; i++)
+ {
+ if(PeerStats.transport[i].ip == ip &&
+ PeerStats.transport[i].type == type &&
+ PeerStats.transport[i].localip == localIP)
+ {
+ addressSaved = true;
+ }
+ }
+ if(addressSaved)
+ continue;
+ PeerStats.transport.push({localip: localIP, ip: ip, type: type});
+ continue;
+ }
+
+ if(now.type == "candidatepair")
+ {
+ if(now.state == "succeeded")
+ continue;
+
+ var local = this.currentStatsReport[now.localCandidateId];
+ var remote = this.currentStatsReport[now.remoteCandidateId];
+ PeerStats.transport.push({localip: local.ipAddress + ":" + local.portNumber,
+ ip: remote.ipAddress + ":" + remote.portNumber, type: local.transport});
+
+ }
+
+ if (now.type != 'ssrc' && now.type != "outboundrtp" &&
+ now.type != "inboundrtp") {
+ continue;
+ }
+
+ var before = this.baselineStatsReport[idx];
+ if (!before) {
+ console.warn(getStatValue(now, 'ssrc') + ' not enough data');
+ continue;
+ }
+
+ var ssrc = getStatValue(now, 'ssrc');
+ if(!ssrc)
+ continue;
+ var jid = APP.xmpp.getJidFromSSRC(ssrc);
+ if (!jid && (Date.now() - now.timestamp) < 3000) {
+ console.warn("No jid for ssrc: " + ssrc);
+ continue;
+ }
+
+ var jidStats = this.jid2stats[jid];
+ if (!jidStats) {
+ jidStats = new PeerStats();
+ this.jid2stats[jid] = jidStats;
+ }
+
+
+ var isDownloadStream = true;
+ var key = 'packetsReceived';
+ if (!getStatValue(now, key))
+ {
+ isDownloadStream = false;
+ key = 'packetsSent';
+ if (!getStatValue(now, key))
+ {
+ console.warn("No packetsReceived nor packetSent stat found");
+ continue;
+ }
+ }
+ var packetsNow = getStatValue(now, key);
+ if(!packetsNow || packetsNow < 0)
+ packetsNow = 0;
+
+ var packetsBefore = getStatValue(before, key);
+ if(!packetsBefore || packetsBefore < 0)
+ packetsBefore = 0;
+ var packetRate = packetsNow - packetsBefore;
+ if(!packetRate || packetRate < 0)
+ packetRate = 0;
+ var currentLoss = getStatValue(now, 'packetsLost');
+ if(!currentLoss || currentLoss < 0)
+ currentLoss = 0;
+ var previousLoss = getStatValue(before, 'packetsLost');
+ if(!previousLoss || previousLoss < 0)
+ previousLoss = 0;
+ var lossRate = currentLoss - previousLoss;
+ if(!lossRate || lossRate < 0)
+ lossRate = 0;
+ var packetsTotal = (packetRate + lossRate);
+
+ jidStats.setSsrcLoss(ssrc,
+ {"packetsTotal": packetsTotal,
+ "packetsLost": lossRate,
+ "isDownloadStream": isDownloadStream});
+
+
+ var bytesReceived = 0, bytesSent = 0;
+ if(getStatValue(now, "bytesReceived"))
+ {
+ bytesReceived = getStatValue(now, "bytesReceived") -
+ getStatValue(before, "bytesReceived");
+ }
+
+ if(getStatValue(now, "bytesSent"))
+ {
+ bytesSent = getStatValue(now, "bytesSent") -
+ getStatValue(before, "bytesSent");
+ }
+
+ var time = Math.round((now.timestamp - before.timestamp) / 1000);
+ if(bytesReceived <= 0 || time <= 0)
+ {
+ bytesReceived = 0;
+ }
+ else
+ {
+ bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
+ }
+
+ if(bytesSent <= 0 || time <= 0)
+ {
+ bytesSent = 0;
+ }
+ else
+ {
+ bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
+ }
+
+ jidStats.setSsrcBitrate(ssrc, {
+ "download": bytesReceived,
+ "upload": bytesSent});
+
+ var resolution = {height: null, width: null};
+ try {
+ if (getStatValue(now, "googFrameHeightReceived") &&
+ getStatValue(now, "googFrameWidthReceived")) {
+ resolution.height = getStatValue(now, "googFrameHeightReceived");
+ resolution.width = getStatValue(now, "googFrameWidthReceived");
+ }
+ else if (getStatValue(now, "googFrameHeightSent") &&
+ getStatValue(now, "googFrameWidthSent")) {
+ resolution.height = getStatValue(now, "googFrameHeightSent");
+ resolution.width = getStatValue(now, "googFrameWidthSent");
+ }
+ }
+ catch(e){/*not supported*/}
+
+ if(resolution.height && resolution.width)
+ {
+ jidStats.setSsrcResolution(ssrc, resolution);
+ }
+ else
+ {
+ jidStats.setSsrcResolution(ssrc, null);
+ }
+
+
+ }
+
+ var self = this;
+ // Jid stats
+ var totalPackets = {download: 0, upload: 0};
+ var lostPackets = {download: 0, upload: 0};
+ var bitrateDownload = 0;
+ var bitrateUpload = 0;
+ var resolutions = {};
+ Object.keys(this.jid2stats).forEach(
+ function (jid)
+ {
+ Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
+ function (ssrc)
+ {
+ var type = "upload";
+ if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream)
+ type = "download";
+ totalPackets[type] +=
+ self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal;
+ lostPackets[type] +=
+ self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost;
+ }
+ );
+ Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach(
+ function (ssrc) {
+ bitrateDownload +=
+ self.jid2stats[jid].ssrc2bitrate[ssrc].download;
+ bitrateUpload +=
+ self.jid2stats[jid].ssrc2bitrate[ssrc].upload;
+
+ delete self.jid2stats[jid].ssrc2bitrate[ssrc];
+ }
+ );
+ resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
+ }
+ );
+
+ PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload};
+
+ PeerStats.packetLoss = {
+ total:
+ calculatePacketLoss(lostPackets.download + lostPackets.upload,
+ totalPackets.download + totalPackets.upload),
+ download:
+ calculatePacketLoss(lostPackets.download, totalPackets.download),
+ upload:
+ calculatePacketLoss(lostPackets.upload, totalPackets.upload)
+ };
+ this.eventEmitter.emit("statistics.connectionstats",
+ {
+ "bitrate": PeerStats.bitrate,
+ "packetLoss": PeerStats.packetLoss,
+ "bandwidth": PeerStats.bandwidth,
+ "resolution": resolutions,
+ "transport": PeerStats.transport
+ });
+ PeerStats.transport = [];
+
+};
+
+/**
+ * Stats processing logic.
+ */
+StatsCollector.prototype.processAudioLevelReport = function ()
+{
+ if (!this.baselineAudioLevelsReport)
+ {
+ return;
+ }
+
+ for (var idx in this.currentAudioLevelsReport)
+ {
+ var now = this.currentAudioLevelsReport[idx];
+
+ if (now.type != 'ssrc')
+ {
+ continue;
+ }
+
+ var before = this.baselineAudioLevelsReport[idx];
+ if (!before)
+ {
+ console.warn(getStatValue(now, 'ssrc') + ' not enough data');
+ continue;
+ }
+
+ var ssrc = getStatValue(now, 'ssrc');
+ var jid = APP.xmpp.getJidFromSSRC(ssrc);
+ if (!jid && (Date.now() - now.timestamp) < 3000)
+ {
+ console.warn("No jid for ssrc: " + ssrc);
+ continue;
+ }
+
+ var jidStats = this.jid2stats[jid];
+ if (!jidStats)
+ {
+ jidStats = new PeerStats();
+ this.jid2stats[jid] = jidStats;
+ }
+
+ // Audio level
+ var audioLevel = null;
+
+ try {
+ audioLevel = getStatValue(now, 'audioInputLevel');
+ if (!audioLevel)
+ audioLevel = getStatValue(now, 'audioOutputLevel');
+ }
+ catch(e) {/*not supported*/
+ console.warn("Audio Levels are not available in the statistics.");
+ clearInterval(this.audioLevelsIntervalId);
+ return;
+ }
+
+ if (audioLevel)
+ {
+ // TODO: can't find specs about what this value really is,
+ // but it seems to vary between 0 and around 32k.
+ audioLevel = audioLevel / 32767;
+ jidStats.setSsrcAudioLevel(ssrc, audioLevel);
+ if(jid != APP.xmpp.myJid())
+ this.eventEmitter.emit("statistics.audioLevel", jid, audioLevel);
+ }
+
+ }
+
+
+};
+
+},{"../../service/RTC/RTCBrowserType":89}],47:[function(require,module,exports){
+/**
+ * Created by hristo on 8/4/14.
+ */
+var LocalStats = require("./LocalStatsCollector.js");
+var RTPStats = require("./RTPStatsCollector.js");
+var EventEmitter = require("events");
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+
+var eventEmitter = new EventEmitter();
+
+var localStats = null;
+
+var rtpStats = null;
+
+function stopLocal()
+{
+ if(localStats)
+ {
+ localStats.stop();
+ localStats = null;
+ }
+}
+
+function stopRemote()
+{
+ if(rtpStats)
+ {
+ rtpStats.stop();
+ eventEmitter.emit("statistics.stop");
+ rtpStats = null;
+ }
+}
+
+function startRemoteStats (peerconnection) {
+ if(rtpStats)
+ {
+ rtpStats.stop();
+ rtpStats = null;
+ }
+
+ rtpStats = new RTPStats(peerconnection, 200, 2000, eventEmitter);
+ rtpStats.start();
+}
+
+function onStreamCreated(stream)
+{
+ if(stream.getOriginalStream().getAudioTracks().length === 0)
+ return;
+
+ localStats = new LocalStats(stream.getOriginalStream(), 200, statistics,
+ eventEmitter);
+ localStats.start();
+}
+
+function onDisposeConference(onUnload) {
+ stopRemote();
+ if(onUnload) {
+ stopLocal();
+ eventEmitter.removeAllListeners();
+ }
+}
+
+
+var statistics =
+{
+ /**
+ * Indicates that this audio level is for local jid.
+ * @type {string}
+ */
+ LOCAL_JID: 'local',
+
+ addAudioLevelListener: function(listener)
+ {
+ eventEmitter.on("statistics.audioLevel", listener);
+ },
+
+ removeAudioLevelListener: function(listener)
+ {
+ eventEmitter.removeListener("statistics.audioLevel", listener);
+ },
+
+ addConnectionStatsListener: function(listener)
+ {
+ eventEmitter.on("statistics.connectionstats", listener);
+ },
+
+ removeConnectionStatsListener: function(listener)
+ {
+ eventEmitter.removeListener("statistics.connectionstats", listener);
+ },
+
+
+ addRemoteStatsStopListener: function(listener)
+ {
+ eventEmitter.on("statistics.stop", listener);
+ },
+
+ removeRemoteStatsStopListener: function(listener)
+ {
+ eventEmitter.removeListener("statistics.stop", listener);
+ },
+
+ stop: function () {
+ stopLocal();
+ stopRemote();
+ if(eventEmitter)
+ {
+ eventEmitter.removeAllListeners();
+ }
+ },
+
+ stopRemoteStatistics: function()
+ {
+ stopRemote();
+ },
+
+ start: function () {
+ APP.RTC.addStreamListener(onStreamCreated,
+ StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
+ APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference);
+ APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
+ startRemoteStats(event.peerconnection);
+ });
+ }
+
+};
+
+
+
+
module.exports = statistics;
-},{"../../service/RTC/StreamEventTypes.js":91,"../../service/xmpp/XMPPEvents":97,"./LocalStatsCollector.js":44,"./RTPStatsCollector.js":45,"events":98}],47:[function(require,module,exports){
-var i18n = require("i18next-client");
-var languages = require("../../service/translation/languages");
-var Settings = require("../settings/Settings");
-var DEFAULT_LANG = languages.EN;
-
-i18n.addPostProcessor("resolveAppName", function(value, key, options) {
- return value.replace("__app__", interfaceConfig.APP_NAME);
-});
-
-
-
-var defaultOptions = {
- detectLngQS: "lang",
- useCookie: false,
- fallbackLng: DEFAULT_LANG,
- load: "unspecific",
- resGetPath: 'lang/__ns__-__lng__.json',
- ns: {
- namespaces: ['main', 'languages'],
- defaultNs: 'main'
- },
- lngWhitelist : languages.getLanguages(),
- fallbackOnNull: true,
- fallbackOnEmpty: true,
- useDataAttrOptions: true,
- defaultValueFromContent: false,
- app: interfaceConfig.APP_NAME,
- getAsync: false,
- defaultValueFromContent: false,
- customLoad: function(lng, ns, options, done) {
- var resPath = "lang/__ns__-__lng__.json";
- if(lng === languages.EN)
- resPath = "lang/__ns__.json";
- var url = i18n.functions.applyReplacement(resPath, { lng: lng, ns: ns });
- i18n.functions.ajax({
- url: url,
- success: function(data, status, xhr) {
- i18n.functions.log('loaded: ' + url);
- done(null, data);
- },
- error : function(xhr, status, error) {
- if ((status && status == 200) ||
- (xhr && xhr.status && xhr.status == 200)) {
- // file loaded but invalid json, stop waste time !
- i18n.functions.error('There is a typo in: ' + url);
- } else if ((status && status == 404) ||
- (xhr && xhr.status && xhr.status == 404)) {
- i18n.functions.log('Does not exist: ' + url);
- } else {
- var theStatus = status ? status :
- ((xhr && xhr.status) ? xhr.status : null);
- i18n.functions.log(theStatus + ' when loading ' + url);
- }
-
- done(error, {});
- },
- dataType: "json",
- async : options.getAsync
- });
- }
- // options for caching
-// useLocalStorage: true,
-// localStorageExpirationTime: 86400000 // in ms, default 1 week
-};
-
-function initCompleted(t)
-{
- $("[data-i18n]").i18n();
-}
-
-function checkForParameter() {
- var query = window.location.search.substring(1);
- var vars = query.split("&");
- for (var i=0;i";
- str += this.translateString(key, options);
- str += "";
- return str;
-
- }
-};
-
-},{"../../service/translation/languages":96,"../settings/Settings":38,"i18next-client":62}],48:[function(require,module,exports){
-/* jshint -W117 */
-var TraceablePeerConnection = require("./TraceablePeerConnection");
-var SDPDiffer = require("./SDPDiffer");
-var SDPUtil = require("./SDPUtil");
-var SDP = require("./SDP");
-var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
-
-// Jingle stuff
-function JingleSession(me, sid, connection, service) {
- this.me = me;
- this.sid = sid;
- this.connection = connection;
- this.initiator = null;
- this.responder = null;
- this.isInitiator = null;
- this.peerjid = null;
- this.state = null;
- this.localSDP = null;
- this.remoteSDP = null;
- this.relayedStreams = [];
- this.startTime = null;
- this.stopTime = null;
- this.media_constraints = null;
- this.pc_constraints = null;
- this.ice_config = {};
- this.drip_container = [];
- this.service = service;
-
- this.usetrickle = true;
- this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
- this.usedrip = false; // dripping is sending trickle candidates not one-by-one
-
- this.hadstuncandidate = false;
- this.hadturncandidate = false;
- this.lasticecandidate = false;
-
- this.statsinterval = null;
-
- this.reason = null;
-
- this.addssrc = [];
- this.removessrc = [];
- this.pendingop = null;
- this.switchstreams = false;
-
- this.wait = true;
- this.localStreamsSSRC = null;
-
- /**
- * The indicator which determines whether the (local) video has been muted
- * in response to a user command in contrast to an automatic decision made
- * by the application logic.
- */
- this.videoMuteByUser = false;
-}
-
-//TODO: this array must be removed when firefox implement multistream support
-JingleSession.notReceivedSSRCs = [];
-
-JingleSession.prototype.initiate = function (peerjid, isInitiator) {
- var self = this;
- if (this.state !== null) {
- console.error('attempt to initiate on session ' + this.sid +
- 'in state ' + this.state);
- return;
- }
- this.isInitiator = isInitiator;
- this.state = 'pending';
- this.initiator = isInitiator ? this.me : peerjid;
- this.responder = !isInitiator ? this.me : peerjid;
- this.peerjid = peerjid;
- this.hadstuncandidate = false;
- this.hadturncandidate = false;
- this.lasticecandidate = false;
-
- this.peerconnection
- = new TraceablePeerConnection(
- this.connection.jingle.ice_config,
- this.connection.jingle.pc_constraints );
-
- this.peerconnection.onicecandidate = function (event) {
- self.sendIceCandidate(event.candidate);
- };
- this.peerconnection.onaddstream = function (event) {
- console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
- self.remoteStreamAdded(event);
- };
- this.peerconnection.onremovestream = function (event) {
- // Remove the stream from remoteStreams
- // FIXME: remotestreamremoved.jingle not defined anywhere(unused)
- $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
- };
- this.peerconnection.onsignalingstatechange = function (event) {
- if (!(self && self.peerconnection)) return;
- };
- this.peerconnection.oniceconnectionstatechange = function (event) {
- if (!(self && self.peerconnection)) return;
- switch (self.peerconnection.iceConnectionState) {
- case 'connected':
- this.startTime = new Date();
- break;
- case 'disconnected':
- this.stopTime = new Date();
- break;
- }
- onIceConnectionStateChange(self.sid, self);
- };
- // add any local and relayed stream
- APP.RTC.localStreams.forEach(function(stream) {
- self.peerconnection.addStream(stream.getOriginalStream());
- });
- this.relayedStreams.forEach(function(stream) {
- self.peerconnection.addStream(stream);
- });
-};
-
-function onIceConnectionStateChange(sid, session) {
- switch (session.peerconnection.iceConnectionState) {
- case 'checking':
- session.timeChecking = (new Date()).getTime();
- session.firstconnect = true;
- break;
- case 'completed': // on caller side
- case 'connected':
- if (session.firstconnect) {
- session.firstconnect = false;
- var metadata = {};
- metadata.setupTime
- = (new Date()).getTime() - session.timeChecking;
- session.peerconnection.getStats(function (res) {
- if(res && res.result) {
- res.result().forEach(function (report) {
- if (report.type == 'googCandidatePair' &&
- report.stat('googActiveConnection') == 'true') {
- metadata.localCandidateType
- = report.stat('googLocalCandidateType');
- metadata.remoteCandidateType
- = report.stat('googRemoteCandidateType');
-
- // log pair as well so we can get nice pie
- // charts
- metadata.candidatePair
- = report.stat('googLocalCandidateType') +
- ';' +
- report.stat('googRemoteCandidateType');
-
- if (report.stat('googRemoteAddress').indexOf('[') === 0)
- {
- metadata.ipv6 = true;
- }
- }
- });
- }
- });
- }
- break;
- }
-}
-
-JingleSession.prototype.accept = function () {
- var self = this;
- this.state = 'active';
-
- var pranswer = this.peerconnection.localDescription;
- if (!pranswer || pranswer.type != 'pranswer') {
- return;
- }
- console.log('going from pranswer to answer');
- if (this.usetrickle) {
- // remove candidates already sent from session-accept
- var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
- for (var i = 0; i < lines.length; i++) {
- pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
- }
- }
- while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
- // FIXME: change any inactive to sendrecv or whatever they were originally
- pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
- }
- pranswer = APP.simulcast.reverseTransformLocalDescription(pranswer);
- var prsdp = new SDP(pranswer.sdp);
- var accept = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-accept',
- initiator: this.initiator,
- responder: this.responder,
- sid: this.sid });
- prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
- var sdp = this.peerconnection.localDescription.sdp;
- while (SDPUtil.find_line(sdp, 'a=inactive')) {
- // FIXME: change any inactive to sendrecv or whatever they were originally
- sdp = sdp.replace('a=inactive', 'a=sendrecv');
- }
- var self = this;
- this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
- function () {
- //console.log('setLocalDescription success');
- self.setLocalDescription();
-
- self.connection.sendIQ(accept,
- function () {
- var ack = {};
- ack.source = 'answer';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName
- }:{};
- error.source = 'answer';
- JingleSession.onJingleError(self.sid, error);
- },
- 10000);
- },
- function (e) {
- console.error('setLocalDescription failed', e);
- }
- );
-};
-
-JingleSession.prototype.terminate = function (reason) {
- this.state = 'ended';
- this.reason = reason;
- this.peerconnection.close();
- if (this.statsinterval !== null) {
- window.clearInterval(this.statsinterval);
- this.statsinterval = null;
- }
-};
-
-JingleSession.prototype.active = function () {
- return this.state == 'active';
-};
-
-JingleSession.prototype.sendIceCandidate = function (candidate) {
- var self = this;
- if (candidate && !this.lasticecandidate) {
- var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
- var jcand = SDPUtil.candidateToJingle(candidate.candidate);
- if (!(ice && jcand)) {
- console.error('failed to get ice && jcand');
- return;
- }
- ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
-
- if (jcand.type === 'srflx') {
- this.hadstuncandidate = true;
- } else if (jcand.type === 'relay') {
- this.hadturncandidate = true;
- }
-
- if (this.usetrickle) {
- if (this.usedrip) {
- if (this.drip_container.length === 0) {
- // start 20ms callout
- window.setTimeout(function () {
- if (self.drip_container.length === 0) return;
- self.sendIceCandidates(self.drip_container);
- self.drip_container = [];
- }, 20);
-
- }
- this.drip_container.push(candidate);
- return;
- } else {
- self.sendIceCandidate([candidate]);
- }
- }
- } else {
- //console.log('sendIceCandidate: last candidate.');
- if (!this.usetrickle) {
- //console.log('should send full offer now...');
- var init = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
- initiator: this.initiator,
- sid: this.sid});
- this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
- var self = this;
- var sendJingle = function (ssrc) {
- if(!ssrc)
- ssrc = {};
- self.localSDP.toJingle(init, self.initiator == self.me ? 'initiator' : 'responder', ssrc);
- self.connection.sendIQ(init,
- function () {
- //console.log('session initiate ack');
- var ack = {};
- ack.source = 'offer';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- self.state = 'error';
- self.peerconnection.close();
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'offer';
- JingleSession.onJingleError(self.sid, error);
- },
- 10000);
- }
- sendJingle();
- }
- this.lasticecandidate = true;
- console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
- console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
-
- if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
- $(document).trigger('nostuncandidates.jingle', [this.sid]);
- }
- }
-};
-
-JingleSession.prototype.sendIceCandidates = function (candidates) {
- console.log('sendIceCandidates', candidates);
- var cand = $iq({to: this.peerjid, type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'transport-info',
- initiator: this.initiator,
- sid: this.sid});
- for (var mid = 0; mid < this.localSDP.media.length; mid++) {
- var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
- var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]);
- if (cands.length > 0) {
- var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
- ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
- cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
- name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
- }).c('transport', ice);
- for (var i = 0; i < cands.length; i++) {
- cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
- }
- // add fingerprint
- if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
- var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
- tmp.required = true;
- cand.c(
- 'fingerprint',
- {xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
- .t(tmp.fingerprint);
- delete tmp.fingerprint;
- cand.attrs(tmp);
- cand.up();
- }
- cand.up(); // transport
- cand.up(); // content
- }
- }
- // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
- //console.log('was this the last candidate', this.lasticecandidate);
- this.connection.sendIQ(cand,
- function () {
- var ack = {};
- ack.source = 'transportinfo';
- $(document).trigger('ack.jingle', [this.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'transportinfo';
- JingleSession.onJingleError(this.sid, error);
- },
- 10000);
-};
-
-
-JingleSession.prototype.sendOffer = function () {
- //console.log('sendOffer...');
- var self = this;
- this.peerconnection.createOffer(function (sdp) {
- self.createdOffer(sdp);
- },
- function (e) {
- console.error('createOffer failed', e);
- },
- this.media_constraints
- );
-};
-
-JingleSession.prototype.createdOffer = function (sdp) {
- //console.log('createdOffer', sdp);
- var self = this;
- this.localSDP = new SDP(sdp.sdp);
- //this.localSDP.mangle();
- var sendJingle = function () {
- var init = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-initiate',
- initiator: this.initiator,
- sid: this.sid});
- self.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
- self.connection.sendIQ(init,
- function () {
- var ack = {};
- ack.source = 'offer';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- self.state = 'error';
- self.peerconnection.close();
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'offer';
- JingleSession.onJingleError(self.sid, error);
- },
- 10000);
- }
- sdp.sdp = this.localSDP.raw;
- this.peerconnection.setLocalDescription(sdp,
- function () {
- if(self.usetrickle)
- {
- sendJingle();
- }
- self.setLocalDescription();
- //console.log('setLocalDescription success');
- },
- function (e) {
- console.error('setLocalDescription failed', e);
- }
- );
- var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
- for (var i = 0; i < cands.length; i++) {
- var cand = SDPUtil.parse_icecandidate(cands[i]);
- if (cand.type == 'srflx') {
- this.hadstuncandidate = true;
- } else if (cand.type == 'relay') {
- this.hadturncandidate = true;
- }
- }
-};
-
-JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
- //console.log('setting remote description... ', desctype);
- this.remoteSDP = new SDP('');
- this.remoteSDP.fromJingle(elem);
- if (this.peerconnection.remoteDescription !== null) {
- console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
- if (this.peerconnection.remoteDescription.type == 'pranswer') {
- var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
- for (var i = 0; i < pranswer.media.length; i++) {
- // make sure we have ice ufrag and pwd
- if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
- if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
- this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
- } else {
- console.warn('no ice ufrag?');
- }
- if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
- this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
- } else {
- console.warn('no ice pwd?');
- }
- }
- // copy over candidates
- var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
- for (var j = 0; j < lines.length; j++) {
- this.remoteSDP.media[i] += lines[j] + '\r\n';
- }
- }
- this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
- }
- }
- var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
-
- this.peerconnection.setRemoteDescription(remotedesc,
- function () {
- //console.log('setRemoteDescription success');
- },
- function (e) {
- console.error('setRemoteDescription error', e);
- JingleSession.onJingleFatalError(self, e);
- }
- );
-};
-
-JingleSession.prototype.addIceCandidate = function (elem) {
- var self = this;
- if (this.peerconnection.signalingState == 'closed') {
- return;
- }
- if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
- console.log('trickle ice candidate arriving before session accept...');
- // create a PRANSWER for setRemoteDescription
- if (!this.remoteSDP) {
- var cobbled = 'v=0\r\n' +
- 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
- 's=-\r\n' +
- 't=0 0\r\n';
- // first, take some things from the local description
- for (var i = 0; i < this.localSDP.media.length; i++) {
- cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
- cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
- if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
- cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
- }
- cobbled += 'a=inactive\r\n';
- }
- this.remoteSDP = new SDP(cobbled);
- }
- // then add things like ice and dtls from remote candidate
- elem.each(function () {
- for (var i = 0; i < self.remoteSDP.media.length; i++) {
- if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
- self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
- if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
- var tmp = $(this).find('transport');
- self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
- self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
- tmp = $(this).find('transport>fingerprint');
- if (tmp.length) {
- self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
- } else {
- console.log('no dtls fingerprint (webrtc issue #1718?)');
- self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
- }
- break;
- }
- }
- }
- });
- this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
-
- // we need a complete SDP with ice-ufrag/ice-pwd in all parts
- // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
- // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
- var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
- return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
- }).length == this.remoteSDP.media.length;
-
- if (iscomplete) {
- console.log('setting pranswer');
- try {
- this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
- function() {
- },
- function(e) {
- console.log('setRemoteDescription pranswer failed', e.toString());
- });
- } catch (e) {
- console.error('setting pranswer failed', e);
- }
- } else {
- //console.log('not yet setting pranswer');
- }
- }
- // operate on each content element
- elem.each(function () {
- // would love to deactivate this, but firefox still requires it
- var idx = -1;
- var i;
- for (i = 0; i < self.remoteSDP.media.length; i++) {
- if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
- self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
- idx = i;
- break;
- }
- }
- if (idx == -1) { // fall back to localdescription
- for (i = 0; i < self.localSDP.media.length; i++) {
- if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
- self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
- idx = i;
- break;
- }
- }
- }
- var name = $(this).attr('name');
- // TODO: check ice-pwd and ice-ufrag?
- $(this).find('transport>candidate').each(function () {
- var line, candidate;
- line = SDPUtil.candidateFromJingle(this);
- candidate = new RTCIceCandidate({sdpMLineIndex: idx,
- sdpMid: name,
- candidate: line});
- try {
- self.peerconnection.addIceCandidate(candidate);
- } catch (e) {
- console.error('addIceCandidate failed', e.toString(), line);
- }
- });
- });
-};
-
-JingleSession.prototype.sendAnswer = function (provisional) {
- //console.log('createAnswer', provisional);
- var self = this;
- this.peerconnection.createAnswer(
- function (sdp) {
- self.createdAnswer(sdp, provisional);
- },
- function (e) {
- console.error('createAnswer failed', e);
- },
- this.media_constraints
- );
-};
-
-JingleSession.prototype.createdAnswer = function (sdp, provisional) {
- //console.log('createAnswer callback');
- var self = this;
- this.localSDP = new SDP(sdp.sdp);
- //this.localSDP.mangle();
- this.usepranswer = provisional === true;
- if (this.usetrickle) {
- if (this.usepranswer) {
- sdp.type = 'pranswer';
- for (var i = 0; i < this.localSDP.media.length; i++) {
- this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
- }
- this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
- }
- }
- var self = this;
- var sendJingle = function (ssrcs) {
-
- var accept = $iq({to: self.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-accept',
- initiator: self.initiator,
- responder: self.responder,
- sid: self.sid });
- var publicLocalDesc = APP.simulcast.reverseTransformLocalDescription(sdp);
- var publicLocalSDP = new SDP(publicLocalDesc.sdp);
- publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
- self.connection.sendIQ(accept,
- function () {
- var ack = {};
- ack.source = 'answer';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'answer';
- JingleSession.onJingleError(self.sid, error);
- },
- 10000);
- }
- sdp.sdp = this.localSDP.raw;
- this.peerconnection.setLocalDescription(sdp,
- function () {
-
- //console.log('setLocalDescription success');
- if (self.usetrickle && !self.usepranswer) {
- sendJingle();
- }
- self.setLocalDescription();
- },
- function (e) {
- console.error('setLocalDescription failed', e);
- }
- );
- var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
- for (var j = 0; j < cands.length; j++) {
- var cand = SDPUtil.parse_icecandidate(cands[j]);
- if (cand.type == 'srflx') {
- this.hadstuncandidate = true;
- } else if (cand.type == 'relay') {
- this.hadturncandidate = true;
- }
- }
-};
-
-JingleSession.prototype.sendTerminate = function (reason, text) {
- var self = this,
- term = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-terminate',
- initiator: this.initiator,
- sid: this.sid})
- .c('reason')
- .c(reason || 'success');
-
- if (text) {
- term.up().c('text').t(text);
- }
-
- this.connection.sendIQ(term,
- function () {
- self.peerconnection.close();
- self.peerconnection = null;
- self.terminate();
- var ack = {};
- ack.source = 'terminate';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- $(document).trigger('ack.jingle', [self.sid, error]);
- },
- 10000);
- if (this.statsinterval !== null) {
- window.clearInterval(this.statsinterval);
- this.statsinterval = null;
- }
-};
-
-JingleSession.prototype.addSource = function (elem, fromJid) {
-
- var self = this;
- // FIXME: dirty waiting
- if (!this.peerconnection.localDescription)
- {
- console.warn("addSource - localDescription not ready yet")
- setTimeout(function()
- {
- self.addSource(elem, fromJid);
- },
- 200
- );
- return;
- }
-
- console.log('addssrc', new Date().getTime());
- console.log('ice', this.peerconnection.iceConnectionState);
- var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
- var mySdp = new SDP(this.peerconnection.localDescription.sdp);
-
- $(elem).each(function (idx, content) {
- var name = $(content).attr('name');
- var lines = '';
- $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
- var semantics = this.getAttribute('semantics');
- var ssrcs = $(this).find('>source').map(function () {
- return this.getAttribute('ssrc');
- }).get();
-
- if (ssrcs.length != 0) {
- lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
- }
- });
- var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
- tmp.each(function () {
- var ssrc = $(this).attr('ssrc');
- if(mySdp.containsSSRC(ssrc)){
- /**
- * This happens when multiple participants change their streams at the same time and
- * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
- * addssrc are scheduled for update IQ. See
- */
- console.warn("Got add stream request for my own ssrc: "+ssrc);
- return;
- }
- $(this).find('>parameter').each(function () {
- lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
- if ($(this).attr('value') && $(this).attr('value').length)
- lines += ':' + $(this).attr('value');
- lines += '\r\n';
- });
- });
- sdp.media.forEach(function(media, idx) {
- if (!SDPUtil.find_line(media, 'a=mid:' + name))
- return;
- sdp.media[idx] += lines;
- if (!self.addssrc[idx]) self.addssrc[idx] = '';
- self.addssrc[idx] += lines;
- });
- sdp.raw = sdp.session + sdp.media.join('');
- });
- this.modifySources();
-};
-
-JingleSession.prototype.removeSource = function (elem, fromJid) {
-
- var self = this;
- // FIXME: dirty waiting
- if (!this.peerconnection.localDescription)
- {
- console.warn("removeSource - localDescription not ready yet")
- setTimeout(function()
- {
- self.removeSource(elem, fromJid);
- },
- 200
- );
- return;
- }
-
- console.log('removessrc', new Date().getTime());
- console.log('ice', this.peerconnection.iceConnectionState);
- var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
- var mySdp = new SDP(this.peerconnection.localDescription.sdp);
-
- $(elem).each(function (idx, content) {
- var name = $(content).attr('name');
- var lines = '';
- $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
- var semantics = this.getAttribute('semantics');
- var ssrcs = $(this).find('>source').map(function () {
- return this.getAttribute('ssrc');
- }).get();
-
- if (ssrcs.length != 0) {
- lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
- }
- });
- var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
- tmp.each(function () {
- var ssrc = $(this).attr('ssrc');
- // This should never happen, but can be useful for bug detection
- if(mySdp.containsSSRC(ssrc)){
- console.error("Got remove stream request for my own ssrc: "+ssrc);
- return;
- }
- $(this).find('>parameter').each(function () {
- lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
- if ($(this).attr('value') && $(this).attr('value').length)
- lines += ':' + $(this).attr('value');
- lines += '\r\n';
- });
- });
- sdp.media.forEach(function(media, idx) {
- if (!SDPUtil.find_line(media, 'a=mid:' + name))
- return;
- sdp.media[idx] += lines;
- if (!self.removessrc[idx]) self.removessrc[idx] = '';
- self.removessrc[idx] += lines;
- });
- sdp.raw = sdp.session + sdp.media.join('');
- });
- this.modifySources();
-};
-
-JingleSession.prototype.modifySources = function (successCallback) {
- var self = this;
- if (this.peerconnection.signalingState == 'closed') return;
- if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
- // There is nothing to do since scheduled job might have been executed by another succeeding call
- this.setLocalDescription();
- if(successCallback){
- successCallback();
- }
- return;
- }
-
- // FIXME: this is a big hack
- // https://code.google.com/p/webrtc/issues/detail?id=2688
- // ^ has been fixed.
- if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
- console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
- this.wait = true;
- window.setTimeout(function() { self.modifySources(successCallback); }, 250);
- return;
- }
- if (this.wait) {
- window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
- this.wait = false;
- return;
- }
-
- // Reset switch streams flag
- this.switchstreams = false;
-
- var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
-
- // add sources
- this.addssrc.forEach(function(lines, idx) {
- sdp.media[idx] += lines;
- });
- this.addssrc = [];
-
- // remove sources
- this.removessrc.forEach(function(lines, idx) {
- lines = lines.split('\r\n');
- lines.pop(); // remove empty last element;
- lines.forEach(function(line) {
- sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
- });
- });
- this.removessrc = [];
-
- // FIXME:
- // this was a hack for the situation when only one peer exists
- // in the conference.
- // check if still required and remove
- if (sdp.media[0])
- sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
- if (sdp.media[1])
- sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
-
- sdp.raw = sdp.session + sdp.media.join('');
- this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
- function() {
-
- if(self.signalingState == 'closed') {
- console.error("createAnswer attempt on closed state");
- return;
- }
-
- self.peerconnection.createAnswer(
- function(modifiedAnswer) {
- // change video direction, see https://github.com/jitsi/jitmeet/issues/41
- if (self.pendingop !== null) {
- var sdp = new SDP(modifiedAnswer.sdp);
- if (sdp.media.length > 1) {
- switch(self.pendingop) {
- case 'mute':
- sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
- break;
- case 'unmute':
- sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
- break;
- }
- sdp.raw = sdp.session + sdp.media.join('');
- modifiedAnswer.sdp = sdp.raw;
- }
- self.pendingop = null;
- }
-
- // FIXME: pushing down an answer while ice connection state
- // is still checking is bad...
- //console.log(self.peerconnection.iceConnectionState);
-
- // trying to work around another chrome bug
- //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
- self.peerconnection.setLocalDescription(modifiedAnswer,
- function() {
- //console.log('modified setLocalDescription ok');
- self.setLocalDescription();
- if(successCallback){
- successCallback();
- }
- },
- function(error) {
- console.error('modified setLocalDescription failed', error);
- }
- );
- },
- function(error) {
- console.error('modified answer failed', error);
- }
- );
- },
- function(error) {
- console.error('modify failed', error);
- }
- );
-};
-
-/**
- * Switches video streams.
- * @param new_stream new stream that will be used as video of this session.
- * @param oldStream old video stream of this session.
- * @param success_callback callback executed after successful stream switch.
- */
-JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
-
- var self = this;
-
- // Remember SDP to figure out added/removed SSRCs
- var oldSdp = null;
- if(self.peerconnection) {
- if(self.peerconnection.localDescription) {
- oldSdp = new SDP(self.peerconnection.localDescription.sdp);
- }
- self.peerconnection.removeStream(oldStream, true);
- self.peerconnection.addStream(new_stream);
- }
-
- APP.RTC.switchVideoStreams(new_stream, oldStream);
-
- // Conference is not active
- if(!oldSdp || !self.peerconnection) {
- success_callback();
- return;
- }
-
- self.switchstreams = true;
- self.modifySources(function() {
- console.log('modify sources done');
-
- success_callback();
-
- var newSdp = new SDP(self.peerconnection.localDescription.sdp);
- console.log("SDPs", oldSdp, newSdp);
- self.notifyMySSRCUpdate(oldSdp, newSdp);
- });
-};
-
-/**
- * Figures out added/removed ssrcs and send update IQs.
- * @param old_sdp SDP object for old description.
- * @param new_sdp SDP object for new description.
- */
-JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
-
- if (!(this.peerconnection.signalingState == 'stable' &&
- this.peerconnection.iceConnectionState == 'connected')){
- console.log("Too early to send updates");
- return;
- }
-
- // send source-remove IQ.
- sdpDiffer = new SDPDiffer(new_sdp, old_sdp);
- var remove = $iq({to: this.peerjid, type: 'set'})
- .c('jingle', {
- xmlns: 'urn:xmpp:jingle:1',
- action: 'source-remove',
- initiator: this.initiator,
- sid: this.sid
- }
- );
- var removed = sdpDiffer.toJingle(remove);
- if (removed) {
- this.connection.sendIQ(remove,
- function (res) {
- console.info('got remove result', res);
- },
- function (err) {
- console.error('got remove error', err);
- }
- );
- } else {
- console.log('removal not necessary');
- }
-
- // send source-add IQ.
- var sdpDiffer = new SDPDiffer(old_sdp, new_sdp);
- var add = $iq({to: this.peerjid, type: 'set'})
- .c('jingle', {
- xmlns: 'urn:xmpp:jingle:1',
- action: 'source-add',
- initiator: this.initiator,
- sid: this.sid
- }
- );
- var added = sdpDiffer.toJingle(add);
- if (added) {
- this.connection.sendIQ(add,
- function (res) {
- console.info('got add result', res);
- },
- function (err) {
- console.error('got add error', err);
- }
- );
- } else {
- console.log('addition not necessary');
- }
-};
-
-/**
- * Determines whether the (local) video is mute i.e. all video tracks are
- * disabled.
- *
- * @return true if the (local) video is mute i.e. all video tracks are
- * disabled; otherwise, false
- */
-JingleSession.prototype.isVideoMute = function () {
- var tracks = APP.RTC.localVideo.getVideoTracks();
- var mute = true;
-
- for (var i = 0; i < tracks.length; ++i) {
- if (tracks[i].enabled) {
- mute = false;
- break;
- }
- }
- return mute;
-};
-
-/**
- * Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
- *
- * @param mute true to mute the (local) video i.e. to disable all video
- * tracks; otherwise, false
- * @param callback a function to be invoked with mute after all video
- * tracks have been enabled/disabled. The function may, optionally, return
- * another function which is to be invoked after the whole mute/unmute operation
- * has completed successfully.
- * @param options an object which specifies optional arguments such as the
- * boolean key byUser with default value true which
- * specifies whether the method was initiated in response to a user command (in
- * contrast to an automatic decision made by the application logic)
- */
-JingleSession.prototype.setVideoMute = function (mute, callback, options) {
- var byUser;
-
- if (options) {
- byUser = options.byUser;
- if (typeof byUser === 'undefined') {
- byUser = true;
- }
- } else {
- byUser = true;
- }
- // The user's command to mute the (local) video takes precedence over any
- // automatic decision made by the application logic.
- if (byUser) {
- this.videoMuteByUser = mute;
- } else if (this.videoMuteByUser) {
- return;
- }
-
- this.hardMuteVideo(mute);
-
- this.modifySources(callback(mute));
-};
-
-// SDP-based mute by going recvonly/sendrecv
-// FIXME: should probably black out the screen as well
-JingleSession.prototype.toggleVideoMute = function (callback) {
- this.service.setVideoMute(APP.RTC.localVideo.isMuted(), callback);
-};
-
-JingleSession.prototype.hardMuteVideo = function (muted) {
- this.pendingop = muted ? 'mute' : 'unmute';
-};
-
-JingleSession.prototype.sendMute = function (muted, content) {
- var info = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-info',
- initiator: this.initiator,
- sid: this.sid });
- info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
- info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
- if (content) {
- info.attrs({'name': content});
- }
- this.connection.send(info);
-};
-
-JingleSession.prototype.sendRinging = function () {
- var info = $iq({to: this.peerjid,
- type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'session-info',
- initiator: this.initiator,
- sid: this.sid });
- info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
- this.connection.send(info);
-};
-
-JingleSession.prototype.getStats = function (interval) {
- var self = this;
- var recv = {audio: 0, video: 0};
- var lost = {audio: 0, video: 0};
- var lastrecv = {audio: 0, video: 0};
- var lastlost = {audio: 0, video: 0};
- var loss = {audio: 0, video: 0};
- var delta = {audio: 0, video: 0};
- this.statsinterval = window.setInterval(function () {
- if (self && self.peerconnection && self.peerconnection.getStats) {
- self.peerconnection.getStats(function (stats) {
- var results = stats.result();
- // TODO: there are so much statistics you can get from this..
- for (var i = 0; i < results.length; ++i) {
- if (results[i].type == 'ssrc') {
- var packetsrecv = results[i].stat('packetsReceived');
- var packetslost = results[i].stat('packetsLost');
- if (packetsrecv && packetslost) {
- packetsrecv = parseInt(packetsrecv, 10);
- packetslost = parseInt(packetslost, 10);
-
- if (results[i].stat('googFrameRateReceived')) {
- lastlost.video = lost.video;
- lastrecv.video = recv.video;
- recv.video = packetsrecv;
- lost.video = packetslost;
- } else {
- lastlost.audio = lost.audio;
- lastrecv.audio = recv.audio;
- recv.audio = packetsrecv;
- lost.audio = packetslost;
- }
- }
- }
- }
- delta.audio = recv.audio - lastrecv.audio;
- delta.video = recv.video - lastrecv.video;
- loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
- loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
- $(document).trigger('packetloss.jingle', [self.sid, loss]);
- });
- }
- }, interval || 3000);
- return this.statsinterval;
-};
-
-JingleSession.onJingleError = function (session, error)
-{
- console.error("Jingle error", error);
-}
-
-JingleSession.onJingleFatalError = function (session, error)
-{
- this.service.sessionTerminated = true;
- this.connection.emuc.doLeave();
- APP.UI.messageHandler.showError("dialog.sorry",
- "dialog.internalError");
-}
-
-JingleSession.prototype.setLocalDescription = function () {
- // put our ssrcs into presence so other clients can identify our stream
- var newssrcs = [];
- var media = APP.simulcast.parseMedia(this.peerconnection.localDescription);
- media.forEach(function (media) {
-
- if(Object.keys(media.sources).length > 0) {
- // TODO(gp) maybe exclude FID streams?
- Object.keys(media.sources).forEach(function (ssrc) {
- newssrcs.push({
- 'ssrc': ssrc,
- 'type': media.type,
- 'direction': media.direction
- });
- });
- }
- else if(this.localStreamsSSRC && this.localStreamsSSRC[media.type])
- {
- newssrcs.push({
- 'ssrc': this.localStreamsSSRC[media.type],
- 'type': media.type,
- 'direction': media.direction
- });
- }
-
- });
-
- console.log('new ssrcs', newssrcs);
-
- // Have to clear presence map to get rid of removed streams
- this.connection.emuc.clearPresenceMedia();
-
- if (newssrcs.length > 0) {
- for (var i = 1; i <= newssrcs.length; i ++) {
- // Change video type to screen
- if (newssrcs[i-1].type === 'video' && APP.desktopsharing.isUsingScreenStream()) {
- newssrcs[i-1].type = 'screen';
- }
- this.connection.emuc.addMediaToPresence(i,
- newssrcs[i-1].type, newssrcs[i-1].ssrc, newssrcs[i-1].direction);
- }
-
- this.connection.emuc.sendPresence();
- }
-}
-
-// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
-function sendKeyframe(pc) {
- console.log('sendkeyframe', pc.iceConnectionState);
- if (pc.iceConnectionState !== 'connected') return; // safe...
- pc.setRemoteDescription(
- pc.remoteDescription,
- function () {
- pc.createAnswer(
- function (modifiedAnswer) {
- pc.setLocalDescription(
- modifiedAnswer,
- function () {
- // noop
- },
- function (error) {
- console.log('triggerKeyframe setLocalDescription failed', error);
- APP.UI.messageHandler.showError();
- }
- );
- },
- function (error) {
- console.log('triggerKeyframe createAnswer failed', error);
- APP.UI.messageHandler.showError();
- }
- );
- },
- function (error) {
- console.log('triggerKeyframe setRemoteDescription failed', error);
- APP.UI.messageHandler.showError();
- }
- );
-}
-
-
-JingleSession.prototype.remoteStreamAdded = function (data, times) {
- var self = this;
- var thessrc;
- var ssrc2jid = this.connection.emuc.ssrc2jid;
-
- // look up an associated JID for a stream id
- if (data.stream.id && data.stream.id.indexOf('mixedmslabel') === -1) {
- // look only at a=ssrc: and _not_ at a=ssrc-group: lines
-
- var ssrclines
- = SDPUtil.find_lines(this.peerconnection.remoteDescription.sdp, 'a=ssrc:');
- ssrclines = ssrclines.filter(function (line) {
- // NOTE(gp) previously we filtered on the mslabel, but that property
- // is not always present.
- // return line.indexOf('mslabel:' + data.stream.label) !== -1;
-
- return ((line.indexOf('msid:' + data.stream.id) !== -1));
- });
- if (ssrclines.length) {
- thessrc = ssrclines[0].substring(7).split(' ')[0];
-
- // We signal our streams (through Jingle to the focus) before we set
- // our presence (through which peers associate remote streams to
- // jids). So, it might arrive that a remote stream is added but
- // ssrc2jid is not yet updated and thus data.peerjid cannot be
- // successfully set. Here we wait for up to a second for the
- // presence to arrive.
-
- if (!ssrc2jid[thessrc]) {
-
- if (typeof times === 'undefined')
- {
- times = 0;
- }
-
- if (times > 10)
- {
- console.warning('Waiting for jid timed out', thessrc);
- }
- else
- {
- setTimeout(function(d) {
- return function() {
- self.remoteStreamAdded(d, times++);
- }
- }(data), 250);
- }
- return;
- }
-
- // ok to overwrite the one from focus? might save work in colibri.js
- console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
- if (ssrc2jid[thessrc]) {
- data.peerjid = ssrc2jid[thessrc];
- }
- }
- }
-
- APP.RTC.createRemoteStream(data, this.sid, thessrc);
-
- var isVideo = data.stream.getVideoTracks().length > 0;
- // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
- if (isVideo &&
- data.peerjid && this.peerjid === data.peerjid &&
- data.stream.getVideoTracks().length === 0 &&
- APP.RTC.localVideo.getTracks().length > 0) {
- window.setTimeout(function () {
- sendKeyframe(self.peerconnection);
- }, 3000);
- }
-}
-
-module.exports = JingleSession;
-
-},{"../../service/RTC/RTCBrowserType":88,"./SDP":49,"./SDPDiffer":50,"./SDPUtil":51,"./TraceablePeerConnection":52}],49:[function(require,module,exports){
-/* jshint -W117 */
-var SDPUtil = require("./SDPUtil");
-
-// SDP STUFF
-function SDP(sdp) {
- this.media = sdp.split('\r\nm=');
- for (var i = 1; i < this.media.length; i++) {
- this.media[i] = 'm=' + this.media[i];
- if (i != this.media.length - 1) {
- this.media[i] += '\r\n';
- }
- }
- this.session = this.media.shift() + '\r\n';
- this.raw = this.session + this.media.join('');
-}
-/**
- * Returns map of MediaChannel mapped per channel idx.
- */
-SDP.prototype.getMediaSsrcMap = function() {
- var self = this;
- var media_ssrcs = {};
- var tmp;
- for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
- tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
- var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
- var media = {
- mediaindex: mediaindex,
- mid: mid,
- ssrcs: {},
- ssrcGroups: []
- };
- media_ssrcs[mediaindex] = media;
- tmp.forEach(function (line) {
- var linessrc = line.substring(7).split(' ')[0];
- // allocate new ChannelSsrc
- if(!media.ssrcs[linessrc]) {
- media.ssrcs[linessrc] = {
- ssrc: linessrc,
- lines: []
- };
- }
- media.ssrcs[linessrc].lines.push(line);
- });
- tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
- tmp.forEach(function(line){
- var semantics = line.substr(0, idx).substr(13);
- var ssrcs = line.substr(14 + semantics.length).split(' ');
- if (ssrcs.length != 0) {
- media.ssrcGroups.push({
- semantics: semantics,
- ssrcs: ssrcs
- });
- }
- });
- }
- return media_ssrcs;
-};
-/**
- * Returns true if this SDP contains given SSRC.
- * @param ssrc the ssrc to check.
- * @returns {boolean} true if this SDP contains given SSRC.
- */
-SDP.prototype.containsSSRC = function(ssrc) {
- var medias = this.getMediaSsrcMap();
- var contains = false;
- Object.keys(medias).forEach(function(mediaindex){
- var media = medias[mediaindex];
- //console.log("Check", channel, ssrc);
- if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){
- contains = true;
- }
- });
- return contains;
-};
-
-
-// remove iSAC and CN from SDP
-SDP.prototype.mangle = function () {
- var i, j, mline, lines, rtpmap, newdesc;
- for (i = 0; i < this.media.length; i++) {
- lines = this.media[i].split('\r\n');
- lines.pop(); // remove empty last element
- mline = SDPUtil.parse_mline(lines.shift());
- if (mline.media != 'audio')
- continue;
- newdesc = '';
- mline.fmt.length = 0;
- for (j = 0; j < lines.length; j++) {
- if (lines[j].substr(0, 9) == 'a=rtpmap:') {
- rtpmap = SDPUtil.parse_rtpmap(lines[j]);
- if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
- continue;
- mline.fmt.push(rtpmap.id);
- newdesc += lines[j] + '\r\n';
- } else {
- newdesc += lines[j] + '\r\n';
- }
- }
- this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
- this.media[i] += newdesc;
- }
- this.raw = this.session + this.media.join('');
-};
-
-// remove lines matching prefix from session section
-SDP.prototype.removeSessionLines = function(prefix) {
- var self = this;
- var lines = SDPUtil.find_lines(this.session, prefix);
- lines.forEach(function(line) {
- self.session = self.session.replace(line + '\r\n', '');
- });
- this.raw = this.session + this.media.join('');
- return lines;
-}
-// remove lines matching prefix from a media section specified by mediaindex
-// TODO: non-numeric mediaindex could match mid
-SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
- var self = this;
- var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
- lines.forEach(function(line) {
- self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
- });
- this.raw = this.session + this.media.join('');
- return lines;
-}
-
-// add content's to a jingle element
-SDP.prototype.toJingle = function (elem, thecreator, ssrcs) {
-// console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
- var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
- var self = this;
- // new bundle plan
- if (SDPUtil.find_line(this.session, 'a=group:')) {
- lines = SDPUtil.find_lines(this.session, 'a=group:');
- for (i = 0; i < lines.length; i++) {
- tmp = lines[i].split(' ');
- var semantics = tmp.shift().substr(8);
- elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
- for (j = 0; j < tmp.length; j++) {
- elem.c('content', {name: tmp[j]}).up();
- }
- elem.up();
- }
- }
- for (i = 0; i < this.media.length; i++) {
- mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
- if (!(mline.media === 'audio' ||
- mline.media === 'video' ||
- mline.media === 'application'))
- {
- continue;
- }
- if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
- ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
- } else {
- if(ssrcs && ssrcs[mline.media])
- {
- ssrc = ssrcs[mline.media];
- }
- else
- ssrc = false;
- }
-
- elem.c('content', {creator: thecreator, name: mline.media});
- if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
- // prefer identifier from a=mid if present
- var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
- elem.attrs({ name: mid });
- }
-
- if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
- {
- elem.c('description',
- {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
- media: mline.media });
- if (ssrc) {
- elem.attrs({ssrc: ssrc});
- }
- for (j = 0; j < mline.fmt.length; j++) {
- rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
- elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
- // put any 'a=fmtp:' + mline.fmt[j] lines into
- if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
- tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
- for (k = 0; k < tmp.length; k++) {
- elem.c('parameter', tmp[k]).up();
- }
- }
- this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
-
- elem.up();
- }
- if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
- elem.c('encryption', {required: 1});
- var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
- crypto.forEach(function(line) {
- elem.c('crypto', SDPUtil.parse_crypto(line)).up();
- });
- elem.up(); // end of encryption
- }
-
- if (ssrc) {
- // new style mapping
- elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- // FIXME: group by ssrc and support multiple different ssrcs
- var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
- if(ssrclines.length > 0) {
- ssrclines.forEach(function (line) {
- idx = line.indexOf(' ');
- var linessrc = line.substr(0, idx).substr(7);
- if (linessrc != ssrc) {
- elem.up();
- ssrc = linessrc;
- elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- }
- var kv = line.substr(idx + 1);
- elem.c('parameter');
- if (kv.indexOf(':') == -1) {
- elem.attrs({ name: kv });
- } else {
- elem.attrs({ name: kv.split(':', 2)[0] });
- elem.attrs({ value: kv.split(':', 2)[1] });
- }
- elem.up();
- });
- elem.up();
- }
- else
- {
- elem.up();
- elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- elem.c('parameter');
- elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)});
- elem.up();
- var msid = null;
- if(mline.media == "audio")
- {
- msid = APP.RTC.localAudio.getId();
- }
- else
- {
- msid = APP.RTC.localVideo.getId();
- }
- if(msid != null)
- {
- msid = msid.replace(/[\{,\}]/g,"");
- elem.c('parameter');
- elem.attrs({name: "msid", value:msid});
- elem.up();
- elem.c('parameter');
- elem.attrs({name: "mslabel", value:msid});
- elem.up();
- elem.c('parameter');
- elem.attrs({name: "label", value:msid});
- elem.up();
- elem.up();
- }
-
-
- }
-
- // XEP-0339 handle ssrc-group attributes
- var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
- ssrc_group_lines.forEach(function(line) {
- idx = line.indexOf(' ');
- var semantics = line.substr(0, idx).substr(13);
- var ssrcs = line.substr(14 + semantics.length).split(' ');
- if (ssrcs.length != 0) {
- elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- ssrcs.forEach(function(ssrc) {
- elem.c('source', { ssrc: ssrc })
- .up();
- });
- elem.up();
- }
- });
- }
-
- if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
- elem.c('rtcp-mux').up();
- }
-
- // XEP-0293 -- map a=rtcp-fb:*
- this.RtcpFbToJingle(i, elem, '*');
-
- // XEP-0294
- if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
- lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
- for (j = 0; j < lines.length; j++) {
- tmp = SDPUtil.parse_extmap(lines[j]);
- elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
- uri: tmp.uri,
- id: tmp.value });
- if (tmp.hasOwnProperty('direction')) {
- switch (tmp.direction) {
- case 'sendonly':
- elem.attrs({senders: 'responder'});
- break;
- case 'recvonly':
- elem.attrs({senders: 'initiator'});
- break;
- case 'sendrecv':
- elem.attrs({senders: 'both'});
- break;
- case 'inactive':
- elem.attrs({senders: 'none'});
- break;
- }
- }
- // TODO: handle params
- elem.up();
- }
- }
- elem.up(); // end of description
- }
-
- // map ice-ufrag/pwd, dtls fingerprint, candidates
- this.TransportToJingle(i, elem);
-
- if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
- elem.attrs({senders: 'both'});
- } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
- elem.attrs({senders: 'initiator'});
- } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
- elem.attrs({senders: 'responder'});
- } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
- elem.attrs({senders: 'none'});
- }
- if (mline.port == '0') {
- // estos hack to reject an m-line
- elem.attrs({senders: 'rejected'});
- }
- elem.up(); // end of content
- }
- elem.up();
- return elem;
-};
-
-SDP.prototype.TransportToJingle = function (mediaindex, elem) {
- var i = mediaindex;
- var tmp;
- var self = this;
- elem.c('transport');
-
- // XEP-0343 DTLS/SCTP
- if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
- {
- var sctpmap = SDPUtil.find_line(
- this.media[i], 'a=sctpmap:', self.session);
- if (sctpmap)
- {
- var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
- elem.c('sctpmap',
- {
- xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
- number: sctpAttrs[0], /* SCTP port */
- protocol: sctpAttrs[1], /* protocol */
- });
- // Optional stream count attribute
- if (sctpAttrs.length > 2)
- elem.attrs({ streams: sctpAttrs[2]});
- elem.up();
- }
- }
- // XEP-0320
- var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
- fingerprints.forEach(function(line) {
- tmp = SDPUtil.parse_fingerprint(line);
- tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
- elem.c('fingerprint').t(tmp.fingerprint);
- delete tmp.fingerprint;
- line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
- if (line) {
- tmp.setup = line.substr(8);
- }
- elem.attrs(tmp);
- elem.up(); // end of fingerprint
- });
- tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
- if (tmp) {
- tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
- elem.attrs(tmp);
- // XEP-0176
- if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
- var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
- lines.forEach(function (line) {
- elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
- });
- }
- }
- elem.up(); // end of transport
-}
-
-SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
- var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
- lines.forEach(function (line) {
- var tmp = SDPUtil.parse_rtcpfb(line);
- if (tmp.type == 'trr-int') {
- elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
- elem.up();
- } else {
- elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
- if (tmp.params.length > 0) {
- elem.attrs({'subtype': tmp.params[0]});
- }
- elem.up();
- }
- });
-};
-
-SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
- var media = '';
- var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
- if (tmp.length) {
- media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
- if (tmp.attr('value')) {
- media += tmp.attr('value');
- } else {
- media += '0';
- }
- media += '\r\n';
- }
- tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
- tmp.each(function () {
- media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
- if ($(this).attr('subtype')) {
- media += ' ' + $(this).attr('subtype');
- }
- media += '\r\n';
- });
- return media;
-};
-
-// construct an SDP from a jingle stanza
-SDP.prototype.fromJingle = function (jingle) {
- var self = this;
- this.raw = 'v=0\r\n' +
- 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
- 's=-\r\n' +
- 't=0 0\r\n';
- // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
- if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
- $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
- var contents = $(group).find('>content').map(function (idx, content) {
- return content.getAttribute('name');
- }).get();
- if (contents.length > 0) {
- self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
- }
- });
- }
-
- this.session = this.raw;
- jingle.find('>content').each(function () {
- var m = self.jingle2media($(this));
- self.media.push(m);
- });
-
- // reconstruct msid-semantic -- apparently not necessary
- /*
- var msid = SDPUtil.parse_ssrc(this.raw);
- if (msid.hasOwnProperty('mslabel')) {
- this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
- }
- */
-
- this.raw = this.session + this.media.join('');
-};
-
-// translate a jingle content element into an an SDP media part
-SDP.prototype.jingle2media = function (content) {
- var media = '',
- desc = content.find('description'),
- ssrc = desc.attr('ssrc'),
- self = this,
- tmp;
- var sctp = content.find(
- '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
-
- tmp = { media: desc.attr('media') };
- tmp.port = '1';
- if (content.attr('senders') == 'rejected') {
- // estos hack to reject an m-line.
- tmp.port = '0';
- }
- if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
- if (sctp.length)
- tmp.proto = 'DTLS/SCTP';
- else
- tmp.proto = 'RTP/SAVPF';
- } else {
- tmp.proto = 'RTP/AVPF';
- }
- if (!sctp.length)
- {
- tmp.fmt = desc.find('payload-type').map(
- function () { return this.getAttribute('id'); }).get();
- media += SDPUtil.build_mline(tmp) + '\r\n';
- }
- else
- {
- media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
- media += 'a=sctpmap:' + sctp.attr('number') +
- ' ' + sctp.attr('protocol');
-
- var streamCount = sctp.attr('streams');
- if (streamCount)
- media += ' ' + streamCount + '\r\n';
- else
- media += '\r\n';
- }
-
- media += 'c=IN IP4 0.0.0.0\r\n';
- if (!sctp.length)
- media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
- tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
- if (tmp.length) {
- if (tmp.attr('ufrag')) {
- media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
- }
- if (tmp.attr('pwd')) {
- media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
- }
- tmp.find('>fingerprint').each(function () {
- // FIXME: check namespace at some point
- media += 'a=fingerprint:' + this.getAttribute('hash');
- media += ' ' + $(this).text();
- media += '\r\n';
- if (this.getAttribute('setup')) {
- media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
- }
- });
- }
- switch (content.attr('senders')) {
- case 'initiator':
- media += 'a=sendonly\r\n';
- break;
- case 'responder':
- media += 'a=recvonly\r\n';
- break;
- case 'none':
- media += 'a=inactive\r\n';
- break;
- case 'both':
- media += 'a=sendrecv\r\n';
- break;
- }
- media += 'a=mid:' + content.attr('name') + '\r\n';
-
- //
- // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
- // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
- if (desc.find('rtcp-mux').length) {
- media += 'a=rtcp-mux\r\n';
- }
-
- if (desc.find('encryption').length) {
- desc.find('encryption>crypto').each(function () {
- media += 'a=crypto:' + this.getAttribute('tag');
- media += ' ' + this.getAttribute('crypto-suite');
- media += ' ' + this.getAttribute('key-params');
- if (this.getAttribute('session-params')) {
- media += ' ' + this.getAttribute('session-params');
- }
- media += '\r\n';
- });
- }
- desc.find('payload-type').each(function () {
- media += SDPUtil.build_rtpmap(this) + '\r\n';
- if ($(this).find('>parameter').length) {
- media += 'a=fmtp:' + this.getAttribute('id') + ' ';
- media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join('; ');
- media += '\r\n';
- }
- // xep-0293
- media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
- });
-
- // xep-0293
- media += self.RtcpFbFromJingle(desc, '*');
-
- // xep-0294
- tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
- tmp.each(function () {
- media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
- });
-
- content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
- media += SDPUtil.candidateFromJingle(this);
- });
-
- // XEP-0339 handle ssrc-group attributes
- tmp = content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
- var semantics = this.getAttribute('semantics');
- var ssrcs = $(this).find('>source').map(function() {
- return this.getAttribute('ssrc');
- }).get();
-
- if (ssrcs.length != 0) {
- media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
- }
- });
-
- tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
- tmp.each(function () {
- var ssrc = this.getAttribute('ssrc');
- $(this).find('>parameter').each(function () {
- media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
- if (this.getAttribute('value') && this.getAttribute('value').length)
- media += ':' + this.getAttribute('value');
- media += '\r\n';
- });
- });
-
- return media;
-};
-
-
-module.exports = SDP;
-
-
-},{"./SDPUtil":51}],50:[function(require,module,exports){
-function SDPDiffer(mySDP, otherSDP) {
- this.mySDP = mySDP;
- this.otherSDP = otherSDP;
-}
-
-/**
- * Returns map of MediaChannel that contains only media not contained in otherSdp . Mapped by channel idx.
- * @param otherSdp the other SDP to check ssrc with.
- */
-SDPDiffer.prototype.getNewMedia = function() {
-
- // this could be useful in Array.prototype.
- 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 (!this[i].equals(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;
- }
-
- var myMedias = this.mySDP.getMediaSsrcMap();
- var othersMedias = this.otherSDP.getMediaSsrcMap();
- var newMedia = {};
- Object.keys(othersMedias).forEach(function(othersMediaIdx) {
- var myMedia = myMedias[othersMediaIdx];
- var othersMedia = othersMedias[othersMediaIdx];
- if(!myMedia && othersMedia) {
- // Add whole channel
- newMedia[othersMediaIdx] = othersMedia;
- return;
- }
- // Look for new ssrcs accross the channel
- Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
- if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
- // Allocate channel if we've found ssrc that doesn't exist in our channel
- if(!newMedia[othersMediaIdx]){
- newMedia[othersMediaIdx] = {
- mediaindex: othersMedia.mediaindex,
- mid: othersMedia.mid,
- ssrcs: {},
- ssrcGroups: []
- };
- }
- newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
- }
- });
-
- // Look for new ssrc groups across the channels
- othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
-
- // try to match the other ssrc-group with an ssrc-group of ours
- var matched = false;
- for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
- var mySsrcGroup = myMedia.ssrcGroups[i];
- if (otherSsrcGroup.semantics == mySsrcGroup.semantics
- && arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
-
- matched = true;
- break;
- }
- }
-
- if (!matched) {
- // Allocate channel if we've found an ssrc-group that doesn't
- // exist in our channel
-
- if(!newMedia[othersMediaIdx]){
- newMedia[othersMediaIdx] = {
- mediaindex: othersMedia.mediaindex,
- mid: othersMedia.mid,
- ssrcs: {},
- ssrcGroups: []
- };
- }
- newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
- }
- });
- });
- return newMedia;
-};
-
-/**
- * Sends SSRC update IQ.
- * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
- * @param sid session identifier that will be put into the IQ.
- * @param initiator initiator identifier.
- * @param toJid destination Jid
- * @param isAdd indicates if this is remove or add operation.
- */
-SDPDiffer.prototype.toJingle = function(modify) {
- var sdpMediaSsrcs = this.getNewMedia();
- var self = this;
-
- // FIXME: only announce video ssrcs since we mix audio and dont need
- // the audio ssrcs therefore
- var modified = false;
- Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
- modified = true;
- var media = sdpMediaSsrcs[mediaindex];
- modify.c('content', {name: media.mid});
-
- modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
- // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
- // generate sources from lines
- Object.keys(media.ssrcs).forEach(function(ssrcNum) {
- var mediaSsrc = media.ssrcs[ssrcNum];
- modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
- modify.attrs({ssrc: mediaSsrc.ssrc});
- // iterate over ssrc lines
- mediaSsrc.lines.forEach(function (line) {
- var idx = line.indexOf(' ');
- var kv = line.substr(idx + 1);
- modify.c('parameter');
- if (kv.indexOf(':') == -1) {
- modify.attrs({ name: kv });
- } else {
- modify.attrs({ name: kv.split(':', 2)[0] });
- modify.attrs({ value: kv.split(':', 2)[1] });
- }
- modify.up(); // end of parameter
- });
- modify.up(); // end of source
- });
-
- // generate source groups from lines
- media.ssrcGroups.forEach(function(ssrcGroup) {
- if (ssrcGroup.ssrcs.length != 0) {
-
- modify.c('ssrc-group', {
- semantics: ssrcGroup.semantics,
- xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
- });
-
- ssrcGroup.ssrcs.forEach(function (ssrc) {
- modify.c('source', { ssrc: ssrc })
- .up(); // end of source
- });
- modify.up(); // end of ssrc-group
- }
- });
-
- modify.up(); // end of description
- modify.up(); // end of content
- });
-
- return modified;
-};
-
+},{"../../service/RTC/StreamEventTypes.js":92,"../../service/xmpp/XMPPEvents":98,"./LocalStatsCollector.js":45,"./RTPStatsCollector.js":46,"events":1}],48:[function(require,module,exports){
+var i18n = require("i18next-client");
+var languages = require("../../service/translation/languages");
+var Settings = require("../settings/Settings");
+var DEFAULT_LANG = languages.EN;
+
+i18n.addPostProcessor("resolveAppName", function(value, key, options) {
+ return value.replace("__app__", interfaceConfig.APP_NAME);
+});
+
+
+
+var defaultOptions = {
+ detectLngQS: "lang",
+ useCookie: false,
+ fallbackLng: DEFAULT_LANG,
+ load: "unspecific",
+ resGetPath: 'lang/__ns__-__lng__.json',
+ ns: {
+ namespaces: ['main', 'languages'],
+ defaultNs: 'main'
+ },
+ lngWhitelist : languages.getLanguages(),
+ fallbackOnNull: true,
+ fallbackOnEmpty: true,
+ useDataAttrOptions: true,
+ defaultValueFromContent: false,
+ app: interfaceConfig.APP_NAME,
+ getAsync: false,
+ defaultValueFromContent: false,
+ customLoad: function(lng, ns, options, done) {
+ var resPath = "lang/__ns__-__lng__.json";
+ if(lng === languages.EN)
+ resPath = "lang/__ns__.json";
+ var url = i18n.functions.applyReplacement(resPath, { lng: lng, ns: ns });
+ i18n.functions.ajax({
+ url: url,
+ success: function(data, status, xhr) {
+ i18n.functions.log('loaded: ' + url);
+ done(null, data);
+ },
+ error : function(xhr, status, error) {
+ if ((status && status == 200) ||
+ (xhr && xhr.status && xhr.status == 200)) {
+ // file loaded but invalid json, stop waste time !
+ i18n.functions.error('There is a typo in: ' + url);
+ } else if ((status && status == 404) ||
+ (xhr && xhr.status && xhr.status == 404)) {
+ i18n.functions.log('Does not exist: ' + url);
+ } else {
+ var theStatus = status ? status :
+ ((xhr && xhr.status) ? xhr.status : null);
+ i18n.functions.log(theStatus + ' when loading ' + url);
+ }
+
+ done(error, {});
+ },
+ dataType: "json",
+ async : options.getAsync
+ });
+ }
+ // options for caching
+// useLocalStorage: true,
+// localStorageExpirationTime: 86400000 // in ms, default 1 week
+};
+
+function initCompleted(t)
+{
+ $("[data-i18n]").i18n();
+}
+
+function checkForParameter() {
+ var query = window.location.search.substring(1);
+ var vars = query.split("&");
+ for (var i=0;i";
+ str += this.translateString(key, options);
+ str += "";
+ return str;
+
+ }
+};
+
+},{"../../service/translation/languages":97,"../settings/Settings":39,"i18next-client":63}],49:[function(require,module,exports){
+/* jshint -W117 */
+var TraceablePeerConnection = require("./TraceablePeerConnection");
+var SDPDiffer = require("./SDPDiffer");
+var SDPUtil = require("./SDPUtil");
+var SDP = require("./SDP");
+var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
+
+// Jingle stuff
+function JingleSession(me, sid, connection, service) {
+ this.me = me;
+ this.sid = sid;
+ this.connection = connection;
+ this.initiator = null;
+ this.responder = null;
+ this.isInitiator = null;
+ this.peerjid = null;
+ this.state = null;
+ this.localSDP = null;
+ this.remoteSDP = null;
+ this.relayedStreams = [];
+ this.startTime = null;
+ this.stopTime = null;
+ this.media_constraints = null;
+ this.pc_constraints = null;
+ this.ice_config = {};
+ this.drip_container = [];
+ this.service = service;
+
+ this.usetrickle = true;
+ this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
+ this.usedrip = false; // dripping is sending trickle candidates not one-by-one
+
+ this.hadstuncandidate = false;
+ this.hadturncandidate = false;
+ this.lasticecandidate = false;
+
+ this.statsinterval = null;
+
+ this.reason = null;
+
+ this.addssrc = [];
+ this.removessrc = [];
+ this.pendingop = null;
+ this.switchstreams = false;
+
+ this.wait = true;
+ this.localStreamsSSRC = null;
+
+ /**
+ * The indicator which determines whether the (local) video has been muted
+ * in response to a user command in contrast to an automatic decision made
+ * by the application logic.
+ */
+ this.videoMuteByUser = false;
+}
+
+//TODO: this array must be removed when firefox implement multistream support
+JingleSession.notReceivedSSRCs = [];
+
+JingleSession.prototype.initiate = function (peerjid, isInitiator) {
+ var self = this;
+ if (this.state !== null) {
+ console.error('attempt to initiate on session ' + this.sid +
+ 'in state ' + this.state);
+ return;
+ }
+ this.isInitiator = isInitiator;
+ this.state = 'pending';
+ this.initiator = isInitiator ? this.me : peerjid;
+ this.responder = !isInitiator ? this.me : peerjid;
+ this.peerjid = peerjid;
+ this.hadstuncandidate = false;
+ this.hadturncandidate = false;
+ this.lasticecandidate = false;
+
+ this.peerconnection
+ = new TraceablePeerConnection(
+ this.connection.jingle.ice_config,
+ this.connection.jingle.pc_constraints );
+
+ this.peerconnection.onicecandidate = function (event) {
+ self.sendIceCandidate(event.candidate);
+ };
+ this.peerconnection.onaddstream = function (event) {
+ console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
+ self.remoteStreamAdded(event);
+ };
+ this.peerconnection.onremovestream = function (event) {
+ // Remove the stream from remoteStreams
+ // FIXME: remotestreamremoved.jingle not defined anywhere(unused)
+ $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
+ };
+ this.peerconnection.onsignalingstatechange = function (event) {
+ if (!(self && self.peerconnection)) return;
+ };
+ this.peerconnection.oniceconnectionstatechange = function (event) {
+ if (!(self && self.peerconnection)) return;
+ switch (self.peerconnection.iceConnectionState) {
+ case 'connected':
+ this.startTime = new Date();
+ break;
+ case 'disconnected':
+ this.stopTime = new Date();
+ break;
+ }
+ onIceConnectionStateChange(self.sid, self);
+ };
+ // add any local and relayed stream
+ APP.RTC.localStreams.forEach(function(stream) {
+ self.peerconnection.addStream(stream.getOriginalStream());
+ });
+ this.relayedStreams.forEach(function(stream) {
+ self.peerconnection.addStream(stream);
+ });
+};
+
+function onIceConnectionStateChange(sid, session) {
+ switch (session.peerconnection.iceConnectionState) {
+ case 'checking':
+ session.timeChecking = (new Date()).getTime();
+ session.firstconnect = true;
+ break;
+ case 'completed': // on caller side
+ case 'connected':
+ if (session.firstconnect) {
+ session.firstconnect = false;
+ var metadata = {};
+ metadata.setupTime
+ = (new Date()).getTime() - session.timeChecking;
+ session.peerconnection.getStats(function (res) {
+ if(res && res.result) {
+ res.result().forEach(function (report) {
+ if (report.type == 'googCandidatePair' &&
+ report.stat('googActiveConnection') == 'true') {
+ metadata.localCandidateType
+ = report.stat('googLocalCandidateType');
+ metadata.remoteCandidateType
+ = report.stat('googRemoteCandidateType');
+
+ // log pair as well so we can get nice pie
+ // charts
+ metadata.candidatePair
+ = report.stat('googLocalCandidateType') +
+ ';' +
+ report.stat('googRemoteCandidateType');
+
+ if (report.stat('googRemoteAddress').indexOf('[') === 0)
+ {
+ metadata.ipv6 = true;
+ }
+ }
+ });
+ }
+ });
+ }
+ break;
+ }
+}
+
+JingleSession.prototype.accept = function () {
+ var self = this;
+ this.state = 'active';
+
+ var pranswer = this.peerconnection.localDescription;
+ if (!pranswer || pranswer.type != 'pranswer') {
+ return;
+ }
+ console.log('going from pranswer to answer');
+ if (this.usetrickle) {
+ // remove candidates already sent from session-accept
+ var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
+ for (var i = 0; i < lines.length; i++) {
+ pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
+ }
+ }
+ while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
+ // FIXME: change any inactive to sendrecv or whatever they were originally
+ pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
+ }
+ pranswer = APP.simulcast.reverseTransformLocalDescription(pranswer);
+ var prsdp = new SDP(pranswer.sdp);
+ var accept = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-accept',
+ initiator: this.initiator,
+ responder: this.responder,
+ sid: this.sid });
+ prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
+ var sdp = this.peerconnection.localDescription.sdp;
+ while (SDPUtil.find_line(sdp, 'a=inactive')) {
+ // FIXME: change any inactive to sendrecv or whatever they were originally
+ sdp = sdp.replace('a=inactive', 'a=sendrecv');
+ }
+ var self = this;
+ this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
+ function () {
+ //console.log('setLocalDescription success');
+ self.setLocalDescription();
+
+ self.connection.sendIQ(accept,
+ function () {
+ var ack = {};
+ ack.source = 'answer';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName
+ }:{};
+ error.source = 'answer';
+ JingleSession.onJingleError(self.sid, error);
+ },
+ 10000);
+ },
+ function (e) {
+ console.error('setLocalDescription failed', e);
+ }
+ );
+};
+
+JingleSession.prototype.terminate = function (reason) {
+ this.state = 'ended';
+ this.reason = reason;
+ this.peerconnection.close();
+ if (this.statsinterval !== null) {
+ window.clearInterval(this.statsinterval);
+ this.statsinterval = null;
+ }
+};
+
+JingleSession.prototype.active = function () {
+ return this.state == 'active';
+};
+
+JingleSession.prototype.sendIceCandidate = function (candidate) {
+ var self = this;
+ if (candidate && !this.lasticecandidate) {
+ var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
+ var jcand = SDPUtil.candidateToJingle(candidate.candidate);
+ if (!(ice && jcand)) {
+ console.error('failed to get ice && jcand');
+ return;
+ }
+ ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+
+ if (jcand.type === 'srflx') {
+ this.hadstuncandidate = true;
+ } else if (jcand.type === 'relay') {
+ this.hadturncandidate = true;
+ }
+
+ if (this.usetrickle) {
+ if (this.usedrip) {
+ if (this.drip_container.length === 0) {
+ // start 20ms callout
+ window.setTimeout(function () {
+ if (self.drip_container.length === 0) return;
+ self.sendIceCandidates(self.drip_container);
+ self.drip_container = [];
+ }, 20);
+
+ }
+ this.drip_container.push(candidate);
+ return;
+ } else {
+ self.sendIceCandidate([candidate]);
+ }
+ }
+ } else {
+ //console.log('sendIceCandidate: last candidate.');
+ if (!this.usetrickle) {
+ //console.log('should send full offer now...');
+ var init = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
+ initiator: this.initiator,
+ sid: this.sid});
+ this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
+ var self = this;
+ var sendJingle = function (ssrc) {
+ if(!ssrc)
+ ssrc = {};
+ self.localSDP.toJingle(init, self.initiator == self.me ? 'initiator' : 'responder', ssrc);
+ self.connection.sendIQ(init,
+ function () {
+ //console.log('session initiate ack');
+ var ack = {};
+ ack.source = 'offer';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ self.state = 'error';
+ self.peerconnection.close();
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'offer';
+ JingleSession.onJingleError(self.sid, error);
+ },
+ 10000);
+ }
+ sendJingle();
+ }
+ this.lasticecandidate = true;
+ console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
+ console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
+
+ if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
+ $(document).trigger('nostuncandidates.jingle', [this.sid]);
+ }
+ }
+};
+
+JingleSession.prototype.sendIceCandidates = function (candidates) {
+ console.log('sendIceCandidates', candidates);
+ var cand = $iq({to: this.peerjid, type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'transport-info',
+ initiator: this.initiator,
+ sid: this.sid});
+ for (var mid = 0; mid < this.localSDP.media.length; mid++) {
+ var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
+ var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]);
+ if (cands.length > 0) {
+ var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
+ ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
+ name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
+ }).c('transport', ice);
+ for (var i = 0; i < cands.length; i++) {
+ cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
+ }
+ // add fingerprint
+ if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
+ var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
+ tmp.required = true;
+ cand.c(
+ 'fingerprint',
+ {xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
+ .t(tmp.fingerprint);
+ delete tmp.fingerprint;
+ cand.attrs(tmp);
+ cand.up();
+ }
+ cand.up(); // transport
+ cand.up(); // content
+ }
+ }
+ // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
+ //console.log('was this the last candidate', this.lasticecandidate);
+ this.connection.sendIQ(cand,
+ function () {
+ var ack = {};
+ ack.source = 'transportinfo';
+ $(document).trigger('ack.jingle', [this.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'transportinfo';
+ JingleSession.onJingleError(this.sid, error);
+ },
+ 10000);
+};
+
+
+JingleSession.prototype.sendOffer = function () {
+ //console.log('sendOffer...');
+ var self = this;
+ this.peerconnection.createOffer(function (sdp) {
+ self.createdOffer(sdp);
+ },
+ function (e) {
+ console.error('createOffer failed', e);
+ },
+ this.media_constraints
+ );
+};
+
+JingleSession.prototype.createdOffer = function (sdp) {
+ //console.log('createdOffer', sdp);
+ var self = this;
+ this.localSDP = new SDP(sdp.sdp);
+ //this.localSDP.mangle();
+ var sendJingle = function () {
+ var init = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-initiate',
+ initiator: this.initiator,
+ sid: this.sid});
+ self.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
+ self.connection.sendIQ(init,
+ function () {
+ var ack = {};
+ ack.source = 'offer';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ self.state = 'error';
+ self.peerconnection.close();
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'offer';
+ JingleSession.onJingleError(self.sid, error);
+ },
+ 10000);
+ }
+ sdp.sdp = this.localSDP.raw;
+ this.peerconnection.setLocalDescription(sdp,
+ function () {
+ if(self.usetrickle)
+ {
+ sendJingle();
+ }
+ self.setLocalDescription();
+ //console.log('setLocalDescription success');
+ },
+ function (e) {
+ console.error('setLocalDescription failed', e);
+ }
+ );
+ var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
+ for (var i = 0; i < cands.length; i++) {
+ var cand = SDPUtil.parse_icecandidate(cands[i]);
+ if (cand.type == 'srflx') {
+ this.hadstuncandidate = true;
+ } else if (cand.type == 'relay') {
+ this.hadturncandidate = true;
+ }
+ }
+};
+
+JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
+ //console.log('setting remote description... ', desctype);
+ this.remoteSDP = new SDP('');
+ this.remoteSDP.fromJingle(elem);
+ if (this.peerconnection.remoteDescription !== null) {
+ console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
+ if (this.peerconnection.remoteDescription.type == 'pranswer') {
+ var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
+ for (var i = 0; i < pranswer.media.length; i++) {
+ // make sure we have ice ufrag and pwd
+ if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
+ if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
+ this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
+ } else {
+ console.warn('no ice ufrag?');
+ }
+ if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
+ this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
+ } else {
+ console.warn('no ice pwd?');
+ }
+ }
+ // copy over candidates
+ var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
+ for (var j = 0; j < lines.length; j++) {
+ this.remoteSDP.media[i] += lines[j] + '\r\n';
+ }
+ }
+ this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
+ }
+ }
+ var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
+
+ this.peerconnection.setRemoteDescription(remotedesc,
+ function () {
+ //console.log('setRemoteDescription success');
+ },
+ function (e) {
+ console.error('setRemoteDescription error', e);
+ JingleSession.onJingleFatalError(self, e);
+ }
+ );
+};
+
+JingleSession.prototype.addIceCandidate = function (elem) {
+ var self = this;
+ if (this.peerconnection.signalingState == 'closed') {
+ return;
+ }
+ if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
+ console.log('trickle ice candidate arriving before session accept...');
+ // create a PRANSWER for setRemoteDescription
+ if (!this.remoteSDP) {
+ var cobbled = 'v=0\r\n' +
+ 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
+ 's=-\r\n' +
+ 't=0 0\r\n';
+ // first, take some things from the local description
+ for (var i = 0; i < this.localSDP.media.length; i++) {
+ cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
+ cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
+ if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
+ cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
+ }
+ cobbled += 'a=inactive\r\n';
+ }
+ this.remoteSDP = new SDP(cobbled);
+ }
+ // then add things like ice and dtls from remote candidate
+ elem.each(function () {
+ for (var i = 0; i < self.remoteSDP.media.length; i++) {
+ if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
+ self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
+ if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
+ var tmp = $(this).find('transport');
+ self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
+ self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
+ tmp = $(this).find('transport>fingerprint');
+ if (tmp.length) {
+ self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
+ } else {
+ console.log('no dtls fingerprint (webrtc issue #1718?)');
+ self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
+ }
+ break;
+ }
+ }
+ }
+ });
+ this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
+
+ // we need a complete SDP with ice-ufrag/ice-pwd in all parts
+ // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
+ // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
+ var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
+ return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
+ }).length == this.remoteSDP.media.length;
+
+ if (iscomplete) {
+ console.log('setting pranswer');
+ try {
+ this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
+ function() {
+ },
+ function(e) {
+ console.log('setRemoteDescription pranswer failed', e.toString());
+ });
+ } catch (e) {
+ console.error('setting pranswer failed', e);
+ }
+ } else {
+ //console.log('not yet setting pranswer');
+ }
+ }
+ // operate on each content element
+ elem.each(function () {
+ // would love to deactivate this, but firefox still requires it
+ var idx = -1;
+ var i;
+ for (i = 0; i < self.remoteSDP.media.length; i++) {
+ if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
+ self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx == -1) { // fall back to localdescription
+ for (i = 0; i < self.localSDP.media.length; i++) {
+ if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
+ self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
+ idx = i;
+ break;
+ }
+ }
+ }
+ var name = $(this).attr('name');
+ // TODO: check ice-pwd and ice-ufrag?
+ $(this).find('transport>candidate').each(function () {
+ var line, candidate;
+ line = SDPUtil.candidateFromJingle(this);
+ candidate = new RTCIceCandidate({sdpMLineIndex: idx,
+ sdpMid: name,
+ candidate: line});
+ try {
+ self.peerconnection.addIceCandidate(candidate);
+ } catch (e) {
+ console.error('addIceCandidate failed', e.toString(), line);
+ }
+ });
+ });
+};
+
+JingleSession.prototype.sendAnswer = function (provisional) {
+ //console.log('createAnswer', provisional);
+ var self = this;
+ this.peerconnection.createAnswer(
+ function (sdp) {
+ self.createdAnswer(sdp, provisional);
+ },
+ function (e) {
+ console.error('createAnswer failed', e);
+ },
+ this.media_constraints
+ );
+};
+
+JingleSession.prototype.createdAnswer = function (sdp, provisional) {
+ //console.log('createAnswer callback');
+ var self = this;
+ this.localSDP = new SDP(sdp.sdp);
+ //this.localSDP.mangle();
+ this.usepranswer = provisional === true;
+ if (this.usetrickle) {
+ if (this.usepranswer) {
+ sdp.type = 'pranswer';
+ for (var i = 0; i < this.localSDP.media.length; i++) {
+ this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
+ }
+ this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
+ }
+ }
+ var self = this;
+ var sendJingle = function (ssrcs) {
+
+ var accept = $iq({to: self.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-accept',
+ initiator: self.initiator,
+ responder: self.responder,
+ sid: self.sid });
+ var publicLocalDesc = APP.simulcast.reverseTransformLocalDescription(sdp);
+ var publicLocalSDP = new SDP(publicLocalDesc.sdp);
+ publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
+ self.connection.sendIQ(accept,
+ function () {
+ var ack = {};
+ ack.source = 'answer';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'answer';
+ JingleSession.onJingleError(self.sid, error);
+ },
+ 10000);
+ }
+ sdp.sdp = this.localSDP.raw;
+ this.peerconnection.setLocalDescription(sdp,
+ function () {
+
+ //console.log('setLocalDescription success');
+ if (self.usetrickle && !self.usepranswer) {
+ sendJingle();
+ }
+ self.setLocalDescription();
+ },
+ function (e) {
+ console.error('setLocalDescription failed', e);
+ }
+ );
+ var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
+ for (var j = 0; j < cands.length; j++) {
+ var cand = SDPUtil.parse_icecandidate(cands[j]);
+ if (cand.type == 'srflx') {
+ this.hadstuncandidate = true;
+ } else if (cand.type == 'relay') {
+ this.hadturncandidate = true;
+ }
+ }
+};
+
+JingleSession.prototype.sendTerminate = function (reason, text) {
+ var self = this,
+ term = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-terminate',
+ initiator: this.initiator,
+ sid: this.sid})
+ .c('reason')
+ .c(reason || 'success');
+
+ if (text) {
+ term.up().c('text').t(text);
+ }
+
+ this.connection.sendIQ(term,
+ function () {
+ self.peerconnection.close();
+ self.peerconnection = null;
+ self.terminate();
+ var ack = {};
+ ack.source = 'terminate';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ $(document).trigger('ack.jingle', [self.sid, error]);
+ },
+ 10000);
+ if (this.statsinterval !== null) {
+ window.clearInterval(this.statsinterval);
+ this.statsinterval = null;
+ }
+};
+
+JingleSession.prototype.addSource = function (elem, fromJid) {
+
+ var self = this;
+ // FIXME: dirty waiting
+ if (!this.peerconnection.localDescription)
+ {
+ console.warn("addSource - localDescription not ready yet")
+ setTimeout(function()
+ {
+ self.addSource(elem, fromJid);
+ },
+ 200
+ );
+ return;
+ }
+
+ console.log('addssrc', new Date().getTime());
+ console.log('ice', this.peerconnection.iceConnectionState);
+ var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
+ var mySdp = new SDP(this.peerconnection.localDescription.sdp);
+
+ $(elem).each(function (idx, content) {
+ var name = $(content).attr('name');
+ var lines = '';
+ $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
+ var semantics = this.getAttribute('semantics');
+ var ssrcs = $(this).find('>source').map(function () {
+ return this.getAttribute('ssrc');
+ }).get();
+
+ if (ssrcs.length != 0) {
+ lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
+ }
+ });
+ var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
+ tmp.each(function () {
+ var ssrc = $(this).attr('ssrc');
+ if(mySdp.containsSSRC(ssrc)){
+ /**
+ * This happens when multiple participants change their streams at the same time and
+ * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
+ * addssrc are scheduled for update IQ. See
+ */
+ console.warn("Got add stream request for my own ssrc: "+ssrc);
+ return;
+ }
+ $(this).find('>parameter').each(function () {
+ lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
+ if ($(this).attr('value') && $(this).attr('value').length)
+ lines += ':' + $(this).attr('value');
+ lines += '\r\n';
+ });
+ });
+ sdp.media.forEach(function(media, idx) {
+ if (!SDPUtil.find_line(media, 'a=mid:' + name))
+ return;
+ sdp.media[idx] += lines;
+ if (!self.addssrc[idx]) self.addssrc[idx] = '';
+ self.addssrc[idx] += lines;
+ });
+ sdp.raw = sdp.session + sdp.media.join('');
+ });
+ this.modifySources();
+};
+
+JingleSession.prototype.removeSource = function (elem, fromJid) {
+
+ var self = this;
+ // FIXME: dirty waiting
+ if (!this.peerconnection.localDescription)
+ {
+ console.warn("removeSource - localDescription not ready yet")
+ setTimeout(function()
+ {
+ self.removeSource(elem, fromJid);
+ },
+ 200
+ );
+ return;
+ }
+
+ console.log('removessrc', new Date().getTime());
+ console.log('ice', this.peerconnection.iceConnectionState);
+ var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
+ var mySdp = new SDP(this.peerconnection.localDescription.sdp);
+
+ $(elem).each(function (idx, content) {
+ var name = $(content).attr('name');
+ var lines = '';
+ $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
+ var semantics = this.getAttribute('semantics');
+ var ssrcs = $(this).find('>source').map(function () {
+ return this.getAttribute('ssrc');
+ }).get();
+
+ if (ssrcs.length != 0) {
+ lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
+ }
+ });
+ var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
+ tmp.each(function () {
+ var ssrc = $(this).attr('ssrc');
+ // This should never happen, but can be useful for bug detection
+ if(mySdp.containsSSRC(ssrc)){
+ console.error("Got remove stream request for my own ssrc: "+ssrc);
+ return;
+ }
+ $(this).find('>parameter').each(function () {
+ lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
+ if ($(this).attr('value') && $(this).attr('value').length)
+ lines += ':' + $(this).attr('value');
+ lines += '\r\n';
+ });
+ });
+ sdp.media.forEach(function(media, idx) {
+ if (!SDPUtil.find_line(media, 'a=mid:' + name))
+ return;
+ sdp.media[idx] += lines;
+ if (!self.removessrc[idx]) self.removessrc[idx] = '';
+ self.removessrc[idx] += lines;
+ });
+ sdp.raw = sdp.session + sdp.media.join('');
+ });
+ this.modifySources();
+};
+
+JingleSession.prototype.modifySources = function (successCallback) {
+ var self = this;
+ if (this.peerconnection.signalingState == 'closed') return;
+ if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
+ // There is nothing to do since scheduled job might have been executed by another succeeding call
+ this.setLocalDescription();
+ if(successCallback){
+ successCallback();
+ }
+ return;
+ }
+
+ // FIXME: this is a big hack
+ // https://code.google.com/p/webrtc/issues/detail?id=2688
+ // ^ has been fixed.
+ if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
+ console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
+ this.wait = true;
+ window.setTimeout(function() { self.modifySources(successCallback); }, 250);
+ return;
+ }
+ if (this.wait) {
+ window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
+ this.wait = false;
+ return;
+ }
+
+ // Reset switch streams flag
+ this.switchstreams = false;
+
+ var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
+
+ // add sources
+ this.addssrc.forEach(function(lines, idx) {
+ sdp.media[idx] += lines;
+ });
+ this.addssrc = [];
+
+ // remove sources
+ this.removessrc.forEach(function(lines, idx) {
+ lines = lines.split('\r\n');
+ lines.pop(); // remove empty last element;
+ lines.forEach(function(line) {
+ sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
+ });
+ });
+ this.removessrc = [];
+
+ // FIXME:
+ // this was a hack for the situation when only one peer exists
+ // in the conference.
+ // check if still required and remove
+ if (sdp.media[0])
+ sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
+ if (sdp.media[1])
+ sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
+
+ sdp.raw = sdp.session + sdp.media.join('');
+ this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
+ function() {
+
+ if(self.signalingState == 'closed') {
+ console.error("createAnswer attempt on closed state");
+ return;
+ }
+
+ self.peerconnection.createAnswer(
+ function(modifiedAnswer) {
+ // change video direction, see https://github.com/jitsi/jitmeet/issues/41
+ if (self.pendingop !== null) {
+ var sdp = new SDP(modifiedAnswer.sdp);
+ if (sdp.media.length > 1) {
+ switch(self.pendingop) {
+ case 'mute':
+ sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
+ break;
+ case 'unmute':
+ sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
+ break;
+ }
+ sdp.raw = sdp.session + sdp.media.join('');
+ modifiedAnswer.sdp = sdp.raw;
+ }
+ self.pendingop = null;
+ }
+
+ // FIXME: pushing down an answer while ice connection state
+ // is still checking is bad...
+ //console.log(self.peerconnection.iceConnectionState);
+
+ // trying to work around another chrome bug
+ //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
+ self.peerconnection.setLocalDescription(modifiedAnswer,
+ function() {
+ //console.log('modified setLocalDescription ok');
+ self.setLocalDescription();
+ if(successCallback){
+ successCallback();
+ }
+ },
+ function(error) {
+ console.error('modified setLocalDescription failed', error);
+ }
+ );
+ },
+ function(error) {
+ console.error('modified answer failed', error);
+ }
+ );
+ },
+ function(error) {
+ console.error('modify failed', error);
+ }
+ );
+};
+
+/**
+ * Switches video streams.
+ * @param new_stream new stream that will be used as video of this session.
+ * @param oldStream old video stream of this session.
+ * @param success_callback callback executed after successful stream switch.
+ */
+JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
+
+ var self = this;
+
+ // Remember SDP to figure out added/removed SSRCs
+ var oldSdp = null;
+ if(self.peerconnection) {
+ if(self.peerconnection.localDescription) {
+ oldSdp = new SDP(self.peerconnection.localDescription.sdp);
+ }
+ self.peerconnection.removeStream(oldStream, true);
+ self.peerconnection.addStream(new_stream);
+ }
+
+ APP.RTC.switchVideoStreams(new_stream, oldStream);
+
+ // Conference is not active
+ if(!oldSdp || !self.peerconnection) {
+ success_callback();
+ return;
+ }
+
+ self.switchstreams = true;
+ self.modifySources(function() {
+ console.log('modify sources done');
+
+ success_callback();
+
+ var newSdp = new SDP(self.peerconnection.localDescription.sdp);
+ console.log("SDPs", oldSdp, newSdp);
+ self.notifyMySSRCUpdate(oldSdp, newSdp);
+ });
+};
+
+/**
+ * Figures out added/removed ssrcs and send update IQs.
+ * @param old_sdp SDP object for old description.
+ * @param new_sdp SDP object for new description.
+ */
+JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
+
+ if (!(this.peerconnection.signalingState == 'stable' &&
+ this.peerconnection.iceConnectionState == 'connected')){
+ console.log("Too early to send updates");
+ return;
+ }
+
+ // send source-remove IQ.
+ sdpDiffer = new SDPDiffer(new_sdp, old_sdp);
+ var remove = $iq({to: this.peerjid, type: 'set'})
+ .c('jingle', {
+ xmlns: 'urn:xmpp:jingle:1',
+ action: 'source-remove',
+ initiator: this.initiator,
+ sid: this.sid
+ }
+ );
+ var removed = sdpDiffer.toJingle(remove);
+ if (removed) {
+ this.connection.sendIQ(remove,
+ function (res) {
+ console.info('got remove result', res);
+ },
+ function (err) {
+ console.error('got remove error', err);
+ }
+ );
+ } else {
+ console.log('removal not necessary');
+ }
+
+ // send source-add IQ.
+ var sdpDiffer = new SDPDiffer(old_sdp, new_sdp);
+ var add = $iq({to: this.peerjid, type: 'set'})
+ .c('jingle', {
+ xmlns: 'urn:xmpp:jingle:1',
+ action: 'source-add',
+ initiator: this.initiator,
+ sid: this.sid
+ }
+ );
+ var added = sdpDiffer.toJingle(add);
+ if (added) {
+ this.connection.sendIQ(add,
+ function (res) {
+ console.info('got add result', res);
+ },
+ function (err) {
+ console.error('got add error', err);
+ }
+ );
+ } else {
+ console.log('addition not necessary');
+ }
+};
+
+/**
+ * Determines whether the (local) video is mute i.e. all video tracks are
+ * disabled.
+ *
+ * @return true if the (local) video is mute i.e. all video tracks are
+ * disabled; otherwise, false
+ */
+JingleSession.prototype.isVideoMute = function () {
+ var tracks = APP.RTC.localVideo.getVideoTracks();
+ var mute = true;
+
+ for (var i = 0; i < tracks.length; ++i) {
+ if (tracks[i].enabled) {
+ mute = false;
+ break;
+ }
+ }
+ return mute;
+};
+
+/**
+ * Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
+ *
+ * @param mute true to mute the (local) video i.e. to disable all video
+ * tracks; otherwise, false
+ * @param callback a function to be invoked with mute after all video
+ * tracks have been enabled/disabled. The function may, optionally, return
+ * another function which is to be invoked after the whole mute/unmute operation
+ * has completed successfully.
+ * @param options an object which specifies optional arguments such as the
+ * boolean key byUser with default value true which
+ * specifies whether the method was initiated in response to a user command (in
+ * contrast to an automatic decision made by the application logic)
+ */
+JingleSession.prototype.setVideoMute = function (mute, callback, options) {
+ var byUser;
+
+ if (options) {
+ byUser = options.byUser;
+ if (typeof byUser === 'undefined') {
+ byUser = true;
+ }
+ } else {
+ byUser = true;
+ }
+ // The user's command to mute the (local) video takes precedence over any
+ // automatic decision made by the application logic.
+ if (byUser) {
+ this.videoMuteByUser = mute;
+ } else if (this.videoMuteByUser) {
+ return;
+ }
+
+ this.hardMuteVideo(mute);
+
+ this.modifySources(callback(mute));
+};
+
+// SDP-based mute by going recvonly/sendrecv
+// FIXME: should probably black out the screen as well
+JingleSession.prototype.toggleVideoMute = function (callback) {
+ this.service.setVideoMute(APP.RTC.localVideo.isMuted(), callback);
+};
+
+JingleSession.prototype.hardMuteVideo = function (muted) {
+ this.pendingop = muted ? 'mute' : 'unmute';
+};
+
+JingleSession.prototype.sendMute = function (muted, content) {
+ var info = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-info',
+ initiator: this.initiator,
+ sid: this.sid });
+ info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
+ info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
+ if (content) {
+ info.attrs({'name': content});
+ }
+ this.connection.send(info);
+};
+
+JingleSession.prototype.sendRinging = function () {
+ var info = $iq({to: this.peerjid,
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'session-info',
+ initiator: this.initiator,
+ sid: this.sid });
+ info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
+ this.connection.send(info);
+};
+
+JingleSession.prototype.getStats = function (interval) {
+ var self = this;
+ var recv = {audio: 0, video: 0};
+ var lost = {audio: 0, video: 0};
+ var lastrecv = {audio: 0, video: 0};
+ var lastlost = {audio: 0, video: 0};
+ var loss = {audio: 0, video: 0};
+ var delta = {audio: 0, video: 0};
+ this.statsinterval = window.setInterval(function () {
+ if (self && self.peerconnection && self.peerconnection.getStats) {
+ self.peerconnection.getStats(function (stats) {
+ var results = stats.result();
+ // TODO: there are so much statistics you can get from this..
+ for (var i = 0; i < results.length; ++i) {
+ if (results[i].type == 'ssrc') {
+ var packetsrecv = results[i].stat('packetsReceived');
+ var packetslost = results[i].stat('packetsLost');
+ if (packetsrecv && packetslost) {
+ packetsrecv = parseInt(packetsrecv, 10);
+ packetslost = parseInt(packetslost, 10);
+
+ if (results[i].stat('googFrameRateReceived')) {
+ lastlost.video = lost.video;
+ lastrecv.video = recv.video;
+ recv.video = packetsrecv;
+ lost.video = packetslost;
+ } else {
+ lastlost.audio = lost.audio;
+ lastrecv.audio = recv.audio;
+ recv.audio = packetsrecv;
+ lost.audio = packetslost;
+ }
+ }
+ }
+ }
+ delta.audio = recv.audio - lastrecv.audio;
+ delta.video = recv.video - lastrecv.video;
+ loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
+ loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
+ $(document).trigger('packetloss.jingle', [self.sid, loss]);
+ });
+ }
+ }, interval || 3000);
+ return this.statsinterval;
+};
+
+JingleSession.onJingleError = function (session, error)
+{
+ console.error("Jingle error", error);
+}
+
+JingleSession.onJingleFatalError = function (session, error)
+{
+ this.service.sessionTerminated = true;
+ this.connection.emuc.doLeave();
+ APP.UI.messageHandler.showError("dialog.sorry",
+ "dialog.internalError");
+}
+
+JingleSession.prototype.setLocalDescription = function () {
+ // put our ssrcs into presence so other clients can identify our stream
+ var newssrcs = [];
+ var media = APP.simulcast.parseMedia(this.peerconnection.localDescription);
+ media.forEach(function (media) {
+
+ if(Object.keys(media.sources).length > 0) {
+ // TODO(gp) maybe exclude FID streams?
+ Object.keys(media.sources).forEach(function (ssrc) {
+ newssrcs.push({
+ 'ssrc': ssrc,
+ 'type': media.type,
+ 'direction': media.direction
+ });
+ });
+ }
+ else if(this.localStreamsSSRC && this.localStreamsSSRC[media.type])
+ {
+ newssrcs.push({
+ 'ssrc': this.localStreamsSSRC[media.type],
+ 'type': media.type,
+ 'direction': media.direction
+ });
+ }
+
+ });
+
+ console.log('new ssrcs', newssrcs);
+
+ // Have to clear presence map to get rid of removed streams
+ this.connection.emuc.clearPresenceMedia();
+
+ if (newssrcs.length > 0) {
+ for (var i = 1; i <= newssrcs.length; i ++) {
+ // Change video type to screen
+ if (newssrcs[i-1].type === 'video' && APP.desktopsharing.isUsingScreenStream()) {
+ newssrcs[i-1].type = 'screen';
+ }
+ this.connection.emuc.addMediaToPresence(i,
+ newssrcs[i-1].type, newssrcs[i-1].ssrc, newssrcs[i-1].direction);
+ }
+
+ this.connection.emuc.sendPresence();
+ }
+}
+
+// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
+function sendKeyframe(pc) {
+ console.log('sendkeyframe', pc.iceConnectionState);
+ if (pc.iceConnectionState !== 'connected') return; // safe...
+ pc.setRemoteDescription(
+ pc.remoteDescription,
+ function () {
+ pc.createAnswer(
+ function (modifiedAnswer) {
+ pc.setLocalDescription(
+ modifiedAnswer,
+ function () {
+ // noop
+ },
+ function (error) {
+ console.log('triggerKeyframe setLocalDescription failed', error);
+ APP.UI.messageHandler.showError();
+ }
+ );
+ },
+ function (error) {
+ console.log('triggerKeyframe createAnswer failed', error);
+ APP.UI.messageHandler.showError();
+ }
+ );
+ },
+ function (error) {
+ console.log('triggerKeyframe setRemoteDescription failed', error);
+ APP.UI.messageHandler.showError();
+ }
+ );
+}
+
+
+JingleSession.prototype.remoteStreamAdded = function (data, times) {
+ var self = this;
+ var thessrc;
+ var ssrc2jid = this.connection.emuc.ssrc2jid;
+
+ // look up an associated JID for a stream id
+ if (data.stream.id && data.stream.id.indexOf('mixedmslabel') === -1) {
+ // look only at a=ssrc: and _not_ at a=ssrc-group: lines
+
+ var ssrclines
+ = SDPUtil.find_lines(this.peerconnection.remoteDescription.sdp, 'a=ssrc:');
+ ssrclines = ssrclines.filter(function (line) {
+ // NOTE(gp) previously we filtered on the mslabel, but that property
+ // is not always present.
+ // return line.indexOf('mslabel:' + data.stream.label) !== -1;
+
+ return ((line.indexOf('msid:' + data.stream.id) !== -1));
+ });
+ if (ssrclines.length) {
+ thessrc = ssrclines[0].substring(7).split(' ')[0];
+
+ // We signal our streams (through Jingle to the focus) before we set
+ // our presence (through which peers associate remote streams to
+ // jids). So, it might arrive that a remote stream is added but
+ // ssrc2jid is not yet updated and thus data.peerjid cannot be
+ // successfully set. Here we wait for up to a second for the
+ // presence to arrive.
+
+ if (!ssrc2jid[thessrc]) {
+
+ if (typeof times === 'undefined')
+ {
+ times = 0;
+ }
+
+ if (times > 10)
+ {
+ console.warning('Waiting for jid timed out', thessrc);
+ }
+ else
+ {
+ setTimeout(function(d) {
+ return function() {
+ self.remoteStreamAdded(d, times++);
+ }
+ }(data), 250);
+ }
+ return;
+ }
+
+ // ok to overwrite the one from focus? might save work in colibri.js
+ console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
+ if (ssrc2jid[thessrc]) {
+ data.peerjid = ssrc2jid[thessrc];
+ }
+ }
+ }
+
+ APP.RTC.createRemoteStream(data, this.sid, thessrc);
+
+ var isVideo = data.stream.getVideoTracks().length > 0;
+ // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
+ if (isVideo &&
+ data.peerjid && this.peerjid === data.peerjid &&
+ data.stream.getVideoTracks().length === 0 &&
+ APP.RTC.localVideo.getTracks().length > 0) {
+ window.setTimeout(function () {
+ sendKeyframe(self.peerconnection);
+ }, 3000);
+ }
+}
+
+module.exports = JingleSession;
+
+},{"../../service/RTC/RTCBrowserType":89,"./SDP":50,"./SDPDiffer":51,"./SDPUtil":52,"./TraceablePeerConnection":53}],50:[function(require,module,exports){
+/* jshint -W117 */
+var SDPUtil = require("./SDPUtil");
+
+// SDP STUFF
+function SDP(sdp) {
+ this.media = sdp.split('\r\nm=');
+ for (var i = 1; i < this.media.length; i++) {
+ this.media[i] = 'm=' + this.media[i];
+ if (i != this.media.length - 1) {
+ this.media[i] += '\r\n';
+ }
+ }
+ this.session = this.media.shift() + '\r\n';
+ this.raw = this.session + this.media.join('');
+}
+/**
+ * Returns map of MediaChannel mapped per channel idx.
+ */
+SDP.prototype.getMediaSsrcMap = function() {
+ var self = this;
+ var media_ssrcs = {};
+ var tmp;
+ for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
+ tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
+ var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
+ var media = {
+ mediaindex: mediaindex,
+ mid: mid,
+ ssrcs: {},
+ ssrcGroups: []
+ };
+ media_ssrcs[mediaindex] = media;
+ tmp.forEach(function (line) {
+ var linessrc = line.substring(7).split(' ')[0];
+ // allocate new ChannelSsrc
+ if(!media.ssrcs[linessrc]) {
+ media.ssrcs[linessrc] = {
+ ssrc: linessrc,
+ lines: []
+ };
+ }
+ media.ssrcs[linessrc].lines.push(line);
+ });
+ tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
+ tmp.forEach(function(line){
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ media.ssrcGroups.push({
+ semantics: semantics,
+ ssrcs: ssrcs
+ });
+ }
+ });
+ }
+ return media_ssrcs;
+};
+/**
+ * Returns true if this SDP contains given SSRC.
+ * @param ssrc the ssrc to check.
+ * @returns {boolean} true if this SDP contains given SSRC.
+ */
+SDP.prototype.containsSSRC = function(ssrc) {
+ var medias = this.getMediaSsrcMap();
+ var contains = false;
+ Object.keys(medias).forEach(function(mediaindex){
+ var media = medias[mediaindex];
+ //console.log("Check", channel, ssrc);
+ if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){
+ contains = true;
+ }
+ });
+ return contains;
+};
+
+
+// remove iSAC and CN from SDP
+SDP.prototype.mangle = function () {
+ var i, j, mline, lines, rtpmap, newdesc;
+ for (i = 0; i < this.media.length; i++) {
+ lines = this.media[i].split('\r\n');
+ lines.pop(); // remove empty last element
+ mline = SDPUtil.parse_mline(lines.shift());
+ if (mline.media != 'audio')
+ continue;
+ newdesc = '';
+ mline.fmt.length = 0;
+ for (j = 0; j < lines.length; j++) {
+ if (lines[j].substr(0, 9) == 'a=rtpmap:') {
+ rtpmap = SDPUtil.parse_rtpmap(lines[j]);
+ if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
+ continue;
+ mline.fmt.push(rtpmap.id);
+ newdesc += lines[j] + '\r\n';
+ } else {
+ newdesc += lines[j] + '\r\n';
+ }
+ }
+ this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
+ this.media[i] += newdesc;
+ }
+ this.raw = this.session + this.media.join('');
+};
+
+// remove lines matching prefix from session section
+SDP.prototype.removeSessionLines = function(prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.session, prefix);
+ lines.forEach(function(line) {
+ self.session = self.session.replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+// remove lines matching prefix from a media section specified by mediaindex
+// TODO: non-numeric mediaindex could match mid
+SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
+ lines.forEach(function(line) {
+ self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+
+// add content's to a jingle element
+SDP.prototype.toJingle = function (elem, thecreator, ssrcs) {
+// console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
+ var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
+ var self = this;
+ // new bundle plan
+ if (SDPUtil.find_line(this.session, 'a=group:')) {
+ lines = SDPUtil.find_lines(this.session, 'a=group:');
+ for (i = 0; i < lines.length; i++) {
+ tmp = lines[i].split(' ');
+ var semantics = tmp.shift().substr(8);
+ elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
+ for (j = 0; j < tmp.length; j++) {
+ elem.c('content', {name: tmp[j]}).up();
+ }
+ elem.up();
+ }
+ }
+ for (i = 0; i < this.media.length; i++) {
+ mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
+ if (!(mline.media === 'audio' ||
+ mline.media === 'video' ||
+ mline.media === 'application'))
+ {
+ continue;
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
+ ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
+ } else {
+ if(ssrcs && ssrcs[mline.media])
+ {
+ ssrc = ssrcs[mline.media];
+ }
+ else
+ ssrc = false;
+ }
+
+ elem.c('content', {creator: thecreator, name: mline.media});
+ if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
+ // prefer identifier from a=mid if present
+ var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
+ elem.attrs({ name: mid });
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
+ {
+ elem.c('description',
+ {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
+ media: mline.media });
+ if (ssrc) {
+ elem.attrs({ssrc: ssrc});
+ }
+ for (j = 0; j < mline.fmt.length; j++) {
+ rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
+ elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
+ // put any 'a=fmtp:' + mline.fmt[j] lines into
+ if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
+ tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
+ for (k = 0; k < tmp.length; k++) {
+ elem.c('parameter', tmp[k]).up();
+ }
+ }
+ this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
+
+ elem.up();
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
+ elem.c('encryption', {required: 1});
+ var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
+ crypto.forEach(function(line) {
+ elem.c('crypto', SDPUtil.parse_crypto(line)).up();
+ });
+ elem.up(); // end of encryption
+ }
+
+ if (ssrc) {
+ // new style mapping
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ // FIXME: group by ssrc and support multiple different ssrcs
+ var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
+ if(ssrclines.length > 0) {
+ ssrclines.forEach(function (line) {
+ idx = line.indexOf(' ');
+ var linessrc = line.substr(0, idx).substr(7);
+ if (linessrc != ssrc) {
+ elem.up();
+ ssrc = linessrc;
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ }
+ var kv = line.substr(idx + 1);
+ elem.c('parameter');
+ if (kv.indexOf(':') == -1) {
+ elem.attrs({ name: kv });
+ } else {
+ elem.attrs({ name: kv.split(':', 2)[0] });
+ elem.attrs({ value: kv.split(':', 2)[1] });
+ }
+ elem.up();
+ });
+ elem.up();
+ }
+ else
+ {
+ elem.up();
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ elem.c('parameter');
+ elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)});
+ elem.up();
+ var msid = null;
+ if(mline.media == "audio")
+ {
+ msid = APP.RTC.localAudio.getId();
+ }
+ else
+ {
+ msid = APP.RTC.localVideo.getId();
+ }
+ if(msid != null)
+ {
+ msid = msid.replace(/[\{,\}]/g,"");
+ elem.c('parameter');
+ elem.attrs({name: "msid", value:msid});
+ elem.up();
+ elem.c('parameter');
+ elem.attrs({name: "mslabel", value:msid});
+ elem.up();
+ elem.c('parameter');
+ elem.attrs({name: "label", value:msid});
+ elem.up();
+ elem.up();
+ }
+
+
+ }
+
+ // XEP-0339 handle ssrc-group attributes
+ var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
+ ssrc_group_lines.forEach(function(line) {
+ idx = line.indexOf(' ');
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ ssrcs.forEach(function(ssrc) {
+ elem.c('source', { ssrc: ssrc })
+ .up();
+ });
+ elem.up();
+ }
+ });
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
+ elem.c('rtcp-mux').up();
+ }
+
+ // XEP-0293 -- map a=rtcp-fb:*
+ this.RtcpFbToJingle(i, elem, '*');
+
+ // XEP-0294
+ if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
+ lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
+ for (j = 0; j < lines.length; j++) {
+ tmp = SDPUtil.parse_extmap(lines[j]);
+ elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
+ uri: tmp.uri,
+ id: tmp.value });
+ if (tmp.hasOwnProperty('direction')) {
+ switch (tmp.direction) {
+ case 'sendonly':
+ elem.attrs({senders: 'responder'});
+ break;
+ case 'recvonly':
+ elem.attrs({senders: 'initiator'});
+ break;
+ case 'sendrecv':
+ elem.attrs({senders: 'both'});
+ break;
+ case 'inactive':
+ elem.attrs({senders: 'none'});
+ break;
+ }
+ }
+ // TODO: handle params
+ elem.up();
+ }
+ }
+ elem.up(); // end of description
+ }
+
+ // map ice-ufrag/pwd, dtls fingerprint, candidates
+ this.TransportToJingle(i, elem);
+
+ if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
+ elem.attrs({senders: 'both'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
+ elem.attrs({senders: 'initiator'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
+ elem.attrs({senders: 'responder'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
+ elem.attrs({senders: 'none'});
+ }
+ if (mline.port == '0') {
+ // estos hack to reject an m-line
+ elem.attrs({senders: 'rejected'});
+ }
+ elem.up(); // end of content
+ }
+ elem.up();
+ return elem;
+};
+
+SDP.prototype.TransportToJingle = function (mediaindex, elem) {
+ var i = mediaindex;
+ var tmp;
+ var self = this;
+ elem.c('transport');
+
+ // XEP-0343 DTLS/SCTP
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
+ {
+ var sctpmap = SDPUtil.find_line(
+ this.media[i], 'a=sctpmap:', self.session);
+ if (sctpmap)
+ {
+ var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
+ elem.c('sctpmap',
+ {
+ xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
+ number: sctpAttrs[0], /* SCTP port */
+ protocol: sctpAttrs[1], /* protocol */
+ });
+ // Optional stream count attribute
+ if (sctpAttrs.length > 2)
+ elem.attrs({ streams: sctpAttrs[2]});
+ elem.up();
+ }
+ }
+ // XEP-0320
+ var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
+ fingerprints.forEach(function(line) {
+ tmp = SDPUtil.parse_fingerprint(line);
+ tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
+ elem.c('fingerprint').t(tmp.fingerprint);
+ delete tmp.fingerprint;
+ line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
+ if (line) {
+ tmp.setup = line.substr(8);
+ }
+ elem.attrs(tmp);
+ elem.up(); // end of fingerprint
+ });
+ tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
+ if (tmp) {
+ tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ elem.attrs(tmp);
+ // XEP-0176
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
+ lines.forEach(function (line) {
+ elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
+ });
+ }
+ }
+ elem.up(); // end of transport
+}
+
+SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
+ lines.forEach(function (line) {
+ var tmp = SDPUtil.parse_rtcpfb(line);
+ if (tmp.type == 'trr-int') {
+ elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
+ elem.up();
+ } else {
+ elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
+ if (tmp.params.length > 0) {
+ elem.attrs({'subtype': tmp.params[0]});
+ }
+ elem.up();
+ }
+ });
+};
+
+SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
+ var media = '';
+ var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ if (tmp.length) {
+ media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
+ if (tmp.attr('value')) {
+ media += tmp.attr('value');
+ } else {
+ media += '0';
+ }
+ media += '\r\n';
+ }
+ tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ tmp.each(function () {
+ media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
+ if ($(this).attr('subtype')) {
+ media += ' ' + $(this).attr('subtype');
+ }
+ media += '\r\n';
+ });
+ return media;
+};
+
+// construct an SDP from a jingle stanza
+SDP.prototype.fromJingle = function (jingle) {
+ var self = this;
+ this.raw = 'v=0\r\n' +
+ 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
+ 's=-\r\n' +
+ 't=0 0\r\n';
+ // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
+ if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
+ $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
+ var contents = $(group).find('>content').map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (contents.length > 0) {
+ self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ }
+
+ this.session = this.raw;
+ jingle.find('>content').each(function () {
+ var m = self.jingle2media($(this));
+ self.media.push(m);
+ });
+
+ // reconstruct msid-semantic -- apparently not necessary
+ /*
+ var msid = SDPUtil.parse_ssrc(this.raw);
+ if (msid.hasOwnProperty('mslabel')) {
+ this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
+ }
+ */
+
+ this.raw = this.session + this.media.join('');
+};
+
+// translate a jingle content element into an an SDP media part
+SDP.prototype.jingle2media = function (content) {
+ var media = '',
+ desc = content.find('description'),
+ ssrc = desc.attr('ssrc'),
+ self = this,
+ tmp;
+ var sctp = content.find(
+ '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
+
+ tmp = { media: desc.attr('media') };
+ tmp.port = '1';
+ if (content.attr('senders') == 'rejected') {
+ // estos hack to reject an m-line.
+ tmp.port = '0';
+ }
+ if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
+ if (sctp.length)
+ tmp.proto = 'DTLS/SCTP';
+ else
+ tmp.proto = 'RTP/SAVPF';
+ } else {
+ tmp.proto = 'RTP/AVPF';
+ }
+ if (!sctp.length)
+ {
+ tmp.fmt = desc.find('payload-type').map(
+ function () { return this.getAttribute('id'); }).get();
+ media += SDPUtil.build_mline(tmp) + '\r\n';
+ }
+ else
+ {
+ media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
+ media += 'a=sctpmap:' + sctp.attr('number') +
+ ' ' + sctp.attr('protocol');
+
+ var streamCount = sctp.attr('streams');
+ if (streamCount)
+ media += ' ' + streamCount + '\r\n';
+ else
+ media += '\r\n';
+ }
+
+ media += 'c=IN IP4 0.0.0.0\r\n';
+ if (!sctp.length)
+ media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
+ tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
+ if (tmp.length) {
+ if (tmp.attr('ufrag')) {
+ media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
+ }
+ if (tmp.attr('pwd')) {
+ media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
+ }
+ tmp.find('>fingerprint').each(function () {
+ // FIXME: check namespace at some point
+ media += 'a=fingerprint:' + this.getAttribute('hash');
+ media += ' ' + $(this).text();
+ media += '\r\n';
+ if (this.getAttribute('setup')) {
+ media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
+ }
+ });
+ }
+ switch (content.attr('senders')) {
+ case 'initiator':
+ media += 'a=sendonly\r\n';
+ break;
+ case 'responder':
+ media += 'a=recvonly\r\n';
+ break;
+ case 'none':
+ media += 'a=inactive\r\n';
+ break;
+ case 'both':
+ media += 'a=sendrecv\r\n';
+ break;
+ }
+ media += 'a=mid:' + content.attr('name') + '\r\n';
+
+ //
+ // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
+ // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
+ if (desc.find('rtcp-mux').length) {
+ media += 'a=rtcp-mux\r\n';
+ }
+
+ if (desc.find('encryption').length) {
+ desc.find('encryption>crypto').each(function () {
+ media += 'a=crypto:' + this.getAttribute('tag');
+ media += ' ' + this.getAttribute('crypto-suite');
+ media += ' ' + this.getAttribute('key-params');
+ if (this.getAttribute('session-params')) {
+ media += ' ' + this.getAttribute('session-params');
+ }
+ media += '\r\n';
+ });
+ }
+ desc.find('payload-type').each(function () {
+ media += SDPUtil.build_rtpmap(this) + '\r\n';
+ if ($(this).find('>parameter').length) {
+ media += 'a=fmtp:' + this.getAttribute('id') + ' ';
+ media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join('; ');
+ media += '\r\n';
+ }
+ // xep-0293
+ media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
+ });
+
+ // xep-0293
+ media += self.RtcpFbFromJingle(desc, '*');
+
+ // xep-0294
+ tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
+ tmp.each(function () {
+ media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
+ });
+
+ content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
+ media += SDPUtil.candidateFromJingle(this);
+ });
+
+ // XEP-0339 handle ssrc-group attributes
+ tmp = content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
+ var semantics = this.getAttribute('semantics');
+ var ssrcs = $(this).find('>source').map(function() {
+ return this.getAttribute('ssrc');
+ }).get();
+
+ if (ssrcs.length != 0) {
+ media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
+ }
+ });
+
+ tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
+ tmp.each(function () {
+ var ssrc = this.getAttribute('ssrc');
+ $(this).find('>parameter').each(function () {
+ media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
+ if (this.getAttribute('value') && this.getAttribute('value').length)
+ media += ':' + this.getAttribute('value');
+ media += '\r\n';
+ });
+ });
+
+ return media;
+};
+
+
+module.exports = SDP;
+
+
+},{"./SDPUtil":52}],51:[function(require,module,exports){
+function SDPDiffer(mySDP, otherSDP) {
+ this.mySDP = mySDP;
+ this.otherSDP = otherSDP;
+}
+
+/**
+ * Returns map of MediaChannel that contains only media not contained in otherSdp . Mapped by channel idx.
+ * @param otherSdp the other SDP to check ssrc with.
+ */
+SDPDiffer.prototype.getNewMedia = function() {
+
+ // this could be useful in Array.prototype.
+ 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 (!this[i].equals(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;
+ }
+
+ var myMedias = this.mySDP.getMediaSsrcMap();
+ var othersMedias = this.otherSDP.getMediaSsrcMap();
+ var newMedia = {};
+ Object.keys(othersMedias).forEach(function(othersMediaIdx) {
+ var myMedia = myMedias[othersMediaIdx];
+ var othersMedia = othersMedias[othersMediaIdx];
+ if(!myMedia && othersMedia) {
+ // Add whole channel
+ newMedia[othersMediaIdx] = othersMedia;
+ return;
+ }
+ // Look for new ssrcs accross the channel
+ Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
+ if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
+ // Allocate channel if we've found ssrc that doesn't exist in our channel
+ if(!newMedia[othersMediaIdx]){
+ newMedia[othersMediaIdx] = {
+ mediaindex: othersMedia.mediaindex,
+ mid: othersMedia.mid,
+ ssrcs: {},
+ ssrcGroups: []
+ };
+ }
+ newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
+ }
+ });
+
+ // Look for new ssrc groups across the channels
+ othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
+
+ // try to match the other ssrc-group with an ssrc-group of ours
+ var matched = false;
+ for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
+ var mySsrcGroup = myMedia.ssrcGroups[i];
+ if (otherSsrcGroup.semantics == mySsrcGroup.semantics
+ && arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
+
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ // Allocate channel if we've found an ssrc-group that doesn't
+ // exist in our channel
+
+ if(!newMedia[othersMediaIdx]){
+ newMedia[othersMediaIdx] = {
+ mediaindex: othersMedia.mediaindex,
+ mid: othersMedia.mid,
+ ssrcs: {},
+ ssrcGroups: []
+ };
+ }
+ newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
+ }
+ });
+ });
+ return newMedia;
+};
+
+/**
+ * Sends SSRC update IQ.
+ * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
+ * @param sid session identifier that will be put into the IQ.
+ * @param initiator initiator identifier.
+ * @param toJid destination Jid
+ * @param isAdd indicates if this is remove or add operation.
+ */
+SDPDiffer.prototype.toJingle = function(modify) {
+ var sdpMediaSsrcs = this.getNewMedia();
+ var self = this;
+
+ // FIXME: only announce video ssrcs since we mix audio and dont need
+ // the audio ssrcs therefore
+ var modified = false;
+ Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
+ modified = true;
+ var media = sdpMediaSsrcs[mediaindex];
+ modify.c('content', {name: media.mid});
+
+ modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
+ // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
+ // generate sources from lines
+ Object.keys(media.ssrcs).forEach(function(ssrcNum) {
+ var mediaSsrc = media.ssrcs[ssrcNum];
+ modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ modify.attrs({ssrc: mediaSsrc.ssrc});
+ // iterate over ssrc lines
+ mediaSsrc.lines.forEach(function (line) {
+ var idx = line.indexOf(' ');
+ var kv = line.substr(idx + 1);
+ modify.c('parameter');
+ if (kv.indexOf(':') == -1) {
+ modify.attrs({ name: kv });
+ } else {
+ modify.attrs({ name: kv.split(':', 2)[0] });
+ modify.attrs({ value: kv.split(':', 2)[1] });
+ }
+ modify.up(); // end of parameter
+ });
+ modify.up(); // end of source
+ });
+
+ // generate source groups from lines
+ media.ssrcGroups.forEach(function(ssrcGroup) {
+ if (ssrcGroup.ssrcs.length != 0) {
+
+ modify.c('ssrc-group', {
+ semantics: ssrcGroup.semantics,
+ xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
+ });
+
+ ssrcGroup.ssrcs.forEach(function (ssrc) {
+ modify.c('source', { ssrc: ssrc })
+ .up(); // end of source
+ });
+ modify.up(); // end of ssrc-group
+ }
+ });
+
+ modify.up(); // end of description
+ modify.up(); // end of content
+ });
+
+ return modified;
+};
+
module.exports = SDPDiffer;
-},{}],51:[function(require,module,exports){
-SDPUtil = {
- iceparams: function (mediadesc, sessiondesc) {
- var data = null;
- if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
- SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
- data = {
- ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
- pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
- };
- }
- return data;
- },
- parse_iceufrag: function (line) {
- return line.substring(12);
- },
- build_iceufrag: function (frag) {
- return 'a=ice-ufrag:' + frag;
- },
- parse_icepwd: function (line) {
- return line.substring(10);
- },
- build_icepwd: function (pwd) {
- return 'a=ice-pwd:' + pwd;
- },
- parse_mid: function (line) {
- return line.substring(6);
- },
- parse_mline: function (line) {
- var parts = line.substring(2).split(' '),
- data = {};
- data.media = parts.shift();
- data.port = parts.shift();
- data.proto = parts.shift();
- if (parts[parts.length - 1] === '') { // trailing whitespace
- parts.pop();
- }
- data.fmt = parts;
- return data;
- },
- build_mline: function (mline) {
- return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
- },
- parse_rtpmap: function (line) {
- var parts = line.substring(9).split(' '),
- data = {};
- data.id = parts.shift();
- parts = parts[0].split('/');
- data.name = parts.shift();
- data.clockrate = parts.shift();
- data.channels = parts.length ? parts.shift() : '1';
- return data;
- },
- /**
- * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
- * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
- * @returns [SCTP port number, protocol, streams]
- */
- parse_sctpmap: function (line)
- {
- var parts = line.substring(10).split(' ');
- var sctpPort = parts[0];
- var protocol = parts[1];
- // Stream count is optional
- var streamCount = parts.length > 2 ? parts[2] : null;
- return [sctpPort, protocol, streamCount];// SCTP port
- },
- build_rtpmap: function (el) {
- var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
- if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
- line += '/' + el.getAttribute('channels');
- }
- return line;
- },
- parse_crypto: function (line) {
- var parts = line.substring(9).split(' '),
- data = {};
- data.tag = parts.shift();
- data['crypto-suite'] = parts.shift();
- data['key-params'] = parts.shift();
- if (parts.length) {
- data['session-params'] = parts.join(' ');
- }
- return data;
- },
- parse_fingerprint: function (line) { // RFC 4572
- var parts = line.substring(14).split(' '),
- data = {};
- data.hash = parts.shift();
- data.fingerprint = parts.shift();
- // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
- return data;
- },
- parse_fmtp: function (line) {
- var parts = line.split(' '),
- i, key, value,
- data = [];
- parts.shift();
- parts = parts.join(' ').split(';');
- for (i = 0; i < parts.length; i++) {
- key = parts[i].split('=')[0];
- while (key.length && key[0] == ' ') {
- key = key.substring(1);
- }
- value = parts[i].split('=')[1];
- if (key && value) {
- data.push({name: key, value: value});
- } else if (key) {
- // rfc 4733 (DTMF) style stuff
- data.push({name: '', value: key});
- }
- }
- return data;
- },
- parse_icecandidate: function (line) {
- var candidate = {},
- elems = line.split(' ');
- candidate.foundation = elems[0].substring(12);
- candidate.component = elems[1];
- candidate.protocol = elems[2].toLowerCase();
- candidate.priority = elems[3];
- candidate.ip = elems[4];
- candidate.port = elems[5];
- // elems[6] => "typ"
- candidate.type = elems[7];
- candidate.generation = 0; // default value, may be overwritten below
- for (var i = 8; i < elems.length; i += 2) {
- switch (elems[i]) {
- case 'raddr':
- candidate['rel-addr'] = elems[i + 1];
- break;
- case 'rport':
- candidate['rel-port'] = elems[i + 1];
- break;
- case 'generation':
- candidate.generation = elems[i + 1];
- break;
- case 'tcptype':
- candidate.tcptype = elems[i + 1];
- break;
- default: // TODO
- console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
- }
- }
- candidate.network = '1';
- candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
- return candidate;
- },
- build_icecandidate: function (cand) {
- var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
- line += ' ';
- switch (cand.type) {
- case 'srflx':
- case 'prflx':
- case 'relay':
- if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
- line += 'raddr';
- line += ' ';
- line += cand['rel-addr'];
- line += ' ';
- line += 'rport';
- line += ' ';
- line += cand['rel-port'];
- line += ' ';
- }
- break;
- }
- if (cand.hasOwnAttribute('tcptype')) {
- line += 'tcptype';
- line += ' ';
- line += cand.tcptype;
- line += ' ';
- }
- line += 'generation';
- line += ' ';
- line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
- return line;
- },
- parse_ssrc: function (desc) {
- // proprietary mapping of a=ssrc lines
- // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
- // and parse according to that
- var lines = desc.split('\r\n'),
- data = {};
- for (var i = 0; i < lines.length; i++) {
- if (lines[i].substring(0, 7) == 'a=ssrc:') {
- var idx = lines[i].indexOf(' ');
- data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
- }
- }
- return data;
- },
- parse_rtcpfb: function (line) {
- var parts = line.substr(10).split(' ');
- var data = {};
- data.pt = parts.shift();
- data.type = parts.shift();
- data.params = parts;
- return data;
- },
- parse_extmap: function (line) {
- var parts = line.substr(9).split(' ');
- var data = {};
- data.value = parts.shift();
- if (data.value.indexOf('/') != -1) {
- data.direction = data.value.substr(data.value.indexOf('/') + 1);
- data.value = data.value.substr(0, data.value.indexOf('/'));
- } else {
- data.direction = 'both';
- }
- data.uri = parts.shift();
- data.params = parts;
- return data;
- },
- find_line: function (haystack, needle, sessionpart) {
- var lines = haystack.split('\r\n');
- for (var i = 0; i < lines.length; i++) {
- if (lines[i].substring(0, needle.length) == needle) {
- return lines[i];
- }
- }
- if (!sessionpart) {
- return false;
- }
- // search session part
- lines = sessionpart.split('\r\n');
- for (var j = 0; j < lines.length; j++) {
- if (lines[j].substring(0, needle.length) == needle) {
- return lines[j];
- }
- }
- return false;
- },
- find_lines: function (haystack, needle, sessionpart) {
- var lines = haystack.split('\r\n'),
- needles = [];
- for (var i = 0; i < lines.length; i++) {
- if (lines[i].substring(0, needle.length) == needle)
- needles.push(lines[i]);
- }
- if (needles.length || !sessionpart) {
- return needles;
- }
- // search session part
- lines = sessionpart.split('\r\n');
- for (var j = 0; j < lines.length; j++) {
- if (lines[j].substring(0, needle.length) == needle) {
- needles.push(lines[j]);
- }
- }
- return needles;
- },
- candidateToJingle: function (line) {
- // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
- //
- if (line.indexOf('candidate:') === 0) {
- line = 'a=' + line;
- } else if (line.substring(0, 12) != 'a=candidate:') {
- console.log('parseCandidate called with a line that is not a candidate line');
- console.log(line);
- return null;
- }
- if (line.substring(line.length - 2) == '\r\n') // chomp it
- line = line.substring(0, line.length - 2);
- var candidate = {},
- elems = line.split(' '),
- i;
- if (elems[6] != 'typ') {
- console.log('did not find typ in the right place');
- console.log(line);
- return null;
- }
- candidate.foundation = elems[0].substring(12);
- candidate.component = elems[1];
- candidate.protocol = elems[2].toLowerCase();
- candidate.priority = elems[3];
- candidate.ip = elems[4];
- candidate.port = elems[5];
- // elems[6] => "typ"
- candidate.type = elems[7];
-
- candidate.generation = '0'; // default, may be overwritten below
- for (i = 8; i < elems.length; i += 2) {
- switch (elems[i]) {
- case 'raddr':
- candidate['rel-addr'] = elems[i + 1];
- break;
- case 'rport':
- candidate['rel-port'] = elems[i + 1];
- break;
- case 'generation':
- candidate.generation = elems[i + 1];
- break;
- case 'tcptype':
- candidate.tcptype = elems[i + 1];
- break;
- default: // TODO
- console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
- }
- }
- candidate.network = '1';
- candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
- return candidate;
- },
- candidateFromJingle: function (cand) {
- var line = 'a=candidate:';
- line += cand.getAttribute('foundation');
- line += ' ';
- line += cand.getAttribute('component');
- line += ' ';
- line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
- line += ' ';
- line += cand.getAttribute('priority');
- line += ' ';
- line += cand.getAttribute('ip');
- line += ' ';
- line += cand.getAttribute('port');
- line += ' ';
- line += 'typ';
- line += ' ' + cand.getAttribute('type');
- line += ' ';
- switch (cand.getAttribute('type')) {
- case 'srflx':
- case 'prflx':
- case 'relay':
- if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
- line += 'raddr';
- line += ' ';
- line += cand.getAttribute('rel-addr');
- line += ' ';
- line += 'rport';
- line += ' ';
- line += cand.getAttribute('rel-port');
- line += ' ';
- }
- break;
- }
- if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
- line += 'tcptype';
- line += ' ';
- line += cand.getAttribute('tcptype');
- line += ' ';
- }
- line += 'generation';
- line += ' ';
- line += cand.getAttribute('generation') || '0';
- return line + '\r\n';
- }
-};
-module.exports = SDPUtil;
},{}],52:[function(require,module,exports){
-function TraceablePeerConnection(ice_config, constraints) {
- var self = this;
- var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
- this.peerconnection = new RTCPeerconnection(ice_config, constraints);
- this.updateLog = [];
- this.stats = {};
- this.statsinterval = null;
- this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
- var Interop = require('sdp-interop').Interop;
- this.interop = new Interop();
-
- // override as desired
- this.trace = function (what, info) {
- //console.warn('WTRACE', what, info);
- self.updateLog.push({
- time: new Date(),
- type: what,
- value: info || ""
- });
- };
- this.onicecandidate = null;
- this.peerconnection.onicecandidate = function (event) {
- self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
- if (self.onicecandidate !== null) {
- self.onicecandidate(event);
- }
- };
- this.onaddstream = null;
- this.peerconnection.onaddstream = function (event) {
- self.trace('onaddstream', event.stream.id);
- if (self.onaddstream !== null) {
- self.onaddstream(event);
- }
- };
- this.onremovestream = null;
- this.peerconnection.onremovestream = function (event) {
- self.trace('onremovestream', event.stream.id);
- if (self.onremovestream !== null) {
- self.onremovestream(event);
- }
- };
- this.onsignalingstatechange = null;
- this.peerconnection.onsignalingstatechange = function (event) {
- self.trace('onsignalingstatechange', self.signalingState);
- if (self.onsignalingstatechange !== null) {
- self.onsignalingstatechange(event);
- }
- };
- this.oniceconnectionstatechange = null;
- this.peerconnection.oniceconnectionstatechange = function (event) {
- self.trace('oniceconnectionstatechange', self.iceConnectionState);
- if (self.oniceconnectionstatechange !== null) {
- self.oniceconnectionstatechange(event);
- }
- };
- this.onnegotiationneeded = null;
- this.peerconnection.onnegotiationneeded = function (event) {
- self.trace('onnegotiationneeded');
- if (self.onnegotiationneeded !== null) {
- self.onnegotiationneeded(event);
- }
- };
- self.ondatachannel = null;
- this.peerconnection.ondatachannel = function (event) {
- self.trace('ondatachannel', event);
- if (self.ondatachannel !== null) {
- self.ondatachannel(event);
- }
- };
- if (!navigator.mozGetUserMedia && this.maxstats) {
- this.statsinterval = window.setInterval(function() {
- self.peerconnection.getStats(function(stats) {
- var results = stats.result();
- for (var i = 0; i < results.length; ++i) {
- //console.log(results[i].type, results[i].id, results[i].names())
- var now = new Date();
- results[i].names().forEach(function (name) {
- var id = results[i].id + '-' + name;
- if (!self.stats[id]) {
- self.stats[id] = {
- startTime: now,
- endTime: now,
- values: [],
- times: []
- };
- }
- self.stats[id].values.push(results[i].stat(name));
- self.stats[id].times.push(now.getTime());
- if (self.stats[id].values.length > self.maxstats) {
- self.stats[id].values.shift();
- self.stats[id].times.shift();
- }
- self.stats[id].endTime = now;
- });
- }
- });
-
- }, 1000);
- }
-};
-
-dumpSDP = function(description) {
- if (typeof description === 'undefined' || description == null) {
- return '';
- }
-
- return 'type: ' + description.type + '\r\n' + description.sdp;
-};
-
-if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
- TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
- TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
- TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
- this.trace('getLocalDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.localDescription));
- // if we're running on FF, transform to Plan B first.
- var desc = this.peerconnection.localDescription;
- if (navigator.mozGetUserMedia) {
- desc = this.interop.toPlanB(desc);
- } else {
- desc = APP.simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription);
- }
- this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
- return desc;
- });
- TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() {
- this.trace('getRemoteDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.remoteDescription));
- // if we're running on FF, transform to Plan B first.
- var desc = this.peerconnection.remoteDescription;
- if (navigator.mozGetUserMedia) {
- desc = this.interop.toPlanB(desc);
- } else {
- desc = APP.simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription);
- }
- this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
- return desc;
- });
-}
-
-TraceablePeerConnection.prototype.addStream = function (stream) {
- this.trace('addStream', stream.id);
- APP.simulcast.resetSender();
- try
- {
- this.peerconnection.addStream(stream);
- }
- catch (e)
- {
- console.error(e);
- return;
- }
-};
-
-TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
- this.trace('removeStream', stream.id);
- APP.simulcast.resetSender();
- if(stopStreams) {
- stream.getAudioTracks().forEach(function (track) {
- track.stop();
- });
- stream.getVideoTracks().forEach(function (track) {
- track.stop();
- });
- }
-
- try {
- // FF doesn't support this yet.
- this.peerconnection.removeStream(stream);
- } catch (e) {
- console.error(e);
- }
-};
-
-TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
- this.trace('createDataChannel', label, opts);
- return this.peerconnection.createDataChannel(label, opts);
-};
-
-TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
- this.trace('setLocalDescription::preTransform (Plan B)', dumpSDP(description));
- // if we're running on FF, transform to Plan A first.
- if (navigator.mozGetUserMedia) {
- description = this.interop.toPlanA(description);
- } else {
- description = APP.simulcast.transformLocalDescription(description);
- }
- this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
- var self = this;
- this.peerconnection.setLocalDescription(description,
- function () {
- self.trace('setLocalDescriptionOnSuccess');
- successCallback();
- },
- function (err) {
- self.trace('setLocalDescriptionOnFailure', err);
- failureCallback(err);
- }
- );
- /*
- if (this.statsinterval === null && this.maxstats > 0) {
- // start gathering stats
- }
- */
-};
-
-TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
- this.trace('setRemoteDescription::preTransform (Plan B)', dumpSDP(description));
- // if we're running on FF, transform to Plan A first.
- if (navigator.mozGetUserMedia) {
- description = this.interop.toPlanA(description);
- }
- else {
- description = APP.simulcast.transformRemoteDescription(description);
- }
- this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
- var self = this;
- this.peerconnection.setRemoteDescription(description,
- function () {
- self.trace('setRemoteDescriptionOnSuccess');
- successCallback();
- },
- function (err) {
- self.trace('setRemoteDescriptionOnFailure', err);
- failureCallback(err);
- }
- );
- /*
- if (this.statsinterval === null && this.maxstats > 0) {
- // start gathering stats
- }
- */
-};
-
-TraceablePeerConnection.prototype.close = function () {
- this.trace('stop');
- if (this.statsinterval !== null) {
- window.clearInterval(this.statsinterval);
- this.statsinterval = null;
- }
- this.peerconnection.close();
-};
-
-TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
- var self = this;
- this.trace('createOffer', JSON.stringify(constraints, null, ' '));
- this.peerconnection.createOffer(
- function (offer) {
- self.trace('createOfferOnSuccess::preTransform (Plan A)', dumpSDP(offer));
- // if we're running on FF, transform to Plan B first.
- if (navigator.mozGetUserMedia) {
- offer = self.interop.toPlanB(offer);
- }
- self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
- successCallback(offer);
- },
- function(err) {
- self.trace('createOfferOnFailure', err);
- failureCallback(err);
- },
- constraints
- );
-};
-
-TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
- var self = this;
- this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
- this.peerconnection.createAnswer(
- function (answer) {
- self.trace('createAnswerOnSuccess::preTransfom (Plan A)', dumpSDP(answer));
- // if we're running on FF, transform to Plan A first.
- if (navigator.mozGetUserMedia) {
- answer = self.interop.toPlanB(answer);
- } else {
- answer = APP.simulcast.transformAnswer(answer);
- }
- self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer));
- successCallback(answer);
- },
- function(err) {
- self.trace('createAnswerOnFailure', err);
- failureCallback(err);
- },
- constraints
- );
-};
-
-TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
- var self = this;
- this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
- this.peerconnection.addIceCandidate(candidate);
- /* maybe later
- this.peerconnection.addIceCandidate(candidate,
- function () {
- self.trace('addIceCandidateOnSuccess');
- successCallback();
- },
- function (err) {
- self.trace('addIceCandidateOnFailure', err);
- failureCallback(err);
- }
- );
- */
-};
-
-TraceablePeerConnection.prototype.getStats = function(callback, errback) {
- if (navigator.mozGetUserMedia) {
- // ignore for now...
- if(!errback)
- errback = function () {
-
- }
- this.peerconnection.getStats(null,callback,errback);
- } else {
- this.peerconnection.getStats(callback);
- }
-};
-
-module.exports = TraceablePeerConnection;
-
-
-},{"sdp-interop":80}],53:[function(require,module,exports){
-/* global $, $iq, APP, config, connection, UI, messageHandler,
- roomName, sessionTerminated, Strophe, Util */
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var Settings = require("../settings/Settings");
-
-var AuthenticationEvents
- = require("../../service/authentication/AuthenticationEvents");
-
-/**
- * Contains logic responsible for enabling/disabling functionality available
- * only to moderator users.
- */
-var connection = null;
-var focusUserJid;
-
-function createExpBackoffTimer(step) {
- var count = 1;
- return function (reset) {
- // Reset call
- if (reset) {
- count = 1;
- return;
- }
- // Calculate next timeout
- var timeout = Math.pow(2, count - 1);
- count += 1;
- return timeout * step;
- };
-}
-
-var getNextTimeout = createExpBackoffTimer(1000);
-var getNextErrorTimeout = createExpBackoffTimer(1000);
-// External authentication stuff
-var externalAuthEnabled = false;
-// Sip gateway can be enabled by configuring Jigasi host in config.js or
-// it will be enabled automatically if focus detects the component through
-// service discovery.
-var sipGatewayEnabled = config.hosts.call_control !== undefined;
-
-var eventEmitter = null;
-
-var Moderator = {
- isModerator: function () {
- return connection && connection.emuc.isModerator();
- },
-
- isPeerModerator: function (peerJid) {
- return connection &&
- connection.emuc.getMemberRole(peerJid) === 'moderator';
- },
-
- isExternalAuthEnabled: function () {
- return externalAuthEnabled;
- },
-
- isSipGatewayEnabled: function () {
- return sipGatewayEnabled;
- },
-
- setConnection: function (con) {
- connection = con;
- },
-
- init: function (xmpp, emitter) {
- this.xmppService = xmpp;
- eventEmitter = emitter;
-
- // Message listener that talks to POPUP window
- function listener(event) {
- if (event.data && event.data.sessionId) {
- if (event.origin !== window.location.origin) {
- console.warn(
- "Ignoring sessionId from different origin: " + event.origin);
- return;
- }
- localStorage.setItem('sessionId', event.data.sessionId);
- // After popup is closed we will authenticate
- }
- }
- // Register
- if (window.addEventListener) {
- window.addEventListener("message", listener, false);
- } else {
- window.attachEvent("onmessage", listener);
- }
- },
-
- onMucLeft: function (jid) {
- console.info("Someone left is it focus ? " + jid);
- var resource = Strophe.getResourceFromJid(jid);
- if (resource === 'focus' && !this.xmppService.sessionTerminated) {
- console.info(
- "Focus has left the room - leaving conference");
- //hangUp();
- // We'd rather reload to have everything re-initialized
- // FIXME: show some message before reload
- location.reload();
- }
- },
-
- setFocusUserJid: function (focusJid) {
- if (!focusUserJid) {
- focusUserJid = focusJid;
- console.info("Focus jid set to: " + focusUserJid);
- }
- },
-
- getFocusUserJid: function () {
- return focusUserJid;
- },
-
- getFocusComponent: function () {
- // Get focus component address
- var focusComponent = config.hosts.focus;
- // If not specified use default: 'focus.domain'
- if (!focusComponent) {
- focusComponent = 'focus.' + config.hosts.domain;
- }
- return focusComponent;
- },
-
- createConferenceIq: function (roomName) {
- // Generate create conference IQ
- var elem = $iq({to: Moderator.getFocusComponent(), type: 'set'});
-
- // Session Id used for authentication
- var sessionId = localStorage.getItem('sessionId');
- var machineUID = Settings.getSettings().uid;
-
- console.info(
- "Session ID: " + sessionId + " machine UID: " + machineUID);
-
- elem.c('conference', {
- xmlns: 'http://jitsi.org/protocol/focus',
- room: roomName,
- 'machine-uid': machineUID
- });
-
- if (sessionId) {
- elem.attrs({ 'session-id': sessionId});
- }
-
- if (config.hosts.bridge !== undefined) {
- elem.c(
- 'property',
- { name: 'bridge', value: config.hosts.bridge})
- .up();
- }
- // Tell the focus we have Jigasi configured
- if (config.hosts.call_control !== undefined) {
- elem.c(
- 'property',
- { name: 'call_control', value: config.hosts.call_control})
- .up();
- }
- if (config.channelLastN !== undefined) {
- elem.c(
- 'property',
- { name: 'channelLastN', value: config.channelLastN})
- .up();
- }
- if (config.adaptiveLastN !== undefined) {
- elem.c(
- 'property',
- { name: 'adaptiveLastN', value: config.adaptiveLastN})
- .up();
- }
- if (config.adaptiveSimulcast !== undefined) {
- elem.c(
- 'property',
- { name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
- .up();
- }
- if (config.openSctp !== undefined) {
- elem.c(
- 'property',
- { name: 'openSctp', value: config.openSctp})
- .up();
- }
- if (config.enableFirefoxSupport !== undefined) {
- elem.c(
- 'property',
- { name: 'enableFirefoxHacks',
- value: config.enableFirefoxSupport})
- .up();
- }
- elem.up();
- return elem;
- },
-
- parseSessionId: function (resultIq) {
- var sessionId = $(resultIq).find('conference').attr('session-id');
- if (sessionId) {
- console.info('Received sessionId: ' + sessionId);
- localStorage.setItem('sessionId', sessionId);
- }
- },
-
- parseConfigOptions: function (resultIq) {
-
- Moderator.setFocusUserJid(
- $(resultIq).find('conference').attr('focusjid'));
-
- var authenticationEnabled
- = $(resultIq).find(
- '>conference>property' +
- '[name=\'authentication\'][value=\'true\']').length > 0;
-
- console.info("Authentication enabled: " + authenticationEnabled);
-
- externalAuthEnabled
- = $(resultIq).find(
- '>conference>property' +
- '[name=\'externalAuth\'][value=\'true\']').length > 0;
-
- console.info('External authentication enabled: ' + externalAuthEnabled);
-
- if (!externalAuthEnabled) {
- // We expect to receive sessionId in 'internal' authentication mode
- Moderator.parseSessionId(resultIq);
- }
-
- var authIdentity = $(resultIq).find('>conference').attr('identity');
-
- eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
- authenticationEnabled, authIdentity);
-
- // Check if focus has auto-detected Jigasi component(this will be also
- // included if we have passed our host from the config)
- if ($(resultIq).find(
- '>conference>property' +
- '[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
- sipGatewayEnabled = true;
- }
-
- console.info("Sip gateway enabled: " + sipGatewayEnabled);
- },
-
- // FIXME: we need to show the fact that we're waiting for the focus
- // to the user(or that focus is not available)
- allocateConferenceFocus: function (roomName, callback) {
- // Try to use focus user JID from the config
- Moderator.setFocusUserJid(config.focusUserJid);
- // Send create conference IQ
- var iq = Moderator.createConferenceIq(roomName);
- var self = this;
- connection.sendIQ(
- iq,
- function (result) {
-
- // Setup config options
- Moderator.parseConfigOptions(result);
-
- if ('true' === $(result).find('conference').attr('ready')) {
- // Reset both timers
- getNextTimeout(true);
- getNextErrorTimeout(true);
- // Exec callback
- callback();
- } else {
- var waitMs = getNextTimeout();
- console.info("Waiting for the focus... " + waitMs);
- // Reset error timeout
- getNextErrorTimeout(true);
- window.setTimeout(
- function () {
- Moderator.allocateConferenceFocus(
- roomName, callback);
- }, waitMs);
- }
- },
- function (error) {
- // Invalid session ? remove and try again
- // without session ID to get a new one
- var invalidSession
- = $(error).find('>error>session-invalid').length;
- if (invalidSession) {
- console.info("Session expired! - removing");
- localStorage.removeItem("sessionId");
- }
- if ($(error).find('>error>graceful-shutdown').length) {
- eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
- return;
- }
- // Check for error returned by the reservation system
- var reservationErr = $(error).find('>error>reservation-error');
- if (reservationErr.length) {
- // Trigger error event
- var errorCode = reservationErr.attr('error-code');
- var errorMsg;
- if ($(error).find('>error>text')) {
- errorMsg = $(error).find('>error>text').text();
- }
- eventEmitter.emit(
- XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
- return;
- }
- // Not authorized to create new room
- if ($(error).find('>error>not-authorized').length) {
- console.warn("Unauthorized to start the conference", error);
- var toDomain
- = Strophe.getDomainFromJid(error.getAttribute('to'));
- if (toDomain !== config.hosts.anonymousdomain) {
- // FIXME: "is external" should come either from
- // the focus or config.js
- externalAuthEnabled = true;
- }
- eventEmitter.emit(
- XMPPEvents.AUTHENTICATION_REQUIRED,
- function () {
- Moderator.allocateConferenceFocus(
- roomName, callback);
- });
- return;
- }
- var waitMs = getNextErrorTimeout();
- console.error("Focus error, retry after " + waitMs, error);
- // Show message
- var focusComponent = Moderator.getFocusComponent();
- var retrySec = waitMs / 1000;
- // FIXME: message is duplicated ?
- // Do not show in case of session invalid
- // which means just a retry
- if (!invalidSession) {
- APP.UI.messageHandler.notify(
- null, "notify.focus",
- 'disconnected', "notify.focusFail",
- {component: focusComponent, ms: retrySec});
- }
- // Reset response timeout
- getNextTimeout(true);
- window.setTimeout(
- function () {
- Moderator.allocateConferenceFocus(roomName, callback);
- }, waitMs);
- }
- );
- },
-
- getLoginUrl: function (roomName, urlCallback) {
- var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
- iq.c('login-url', {
- xmlns: 'http://jitsi.org/protocol/focus',
- room: roomName,
- 'machine-uid': Settings.getSettings().uid
- });
- connection.sendIQ(
- iq,
- function (result) {
- var url = $(result).find('login-url').attr('url');
- url = url = decodeURIComponent(url);
- if (url) {
- console.info("Got auth url: " + url);
- urlCallback(url);
- } else {
- console.error(
- "Failed to get auth url from the focus", result);
- }
- },
- function (error) {
- console.error("Get auth url error", error);
- }
- );
- },
- getPopupLoginUrl: function (roomName, urlCallback) {
- var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
- iq.c('login-url', {
- xmlns: 'http://jitsi.org/protocol/focus',
- room: roomName,
- 'machine-uid': Settings.getSettings().uid,
- popup: true
- });
- connection.sendIQ(
- iq,
- function (result) {
- var url = $(result).find('login-url').attr('url');
- url = url = decodeURIComponent(url);
- if (url) {
- console.info("Got POPUP auth url: " + url);
- urlCallback(url);
- } else {
- console.error(
- "Failed to get POPUP auth url from the focus", result);
- }
- },
- function (error) {
- console.error('Get POPUP auth url error', error);
- }
- );
- },
- logout: function (callback) {
- var iq = $iq({to: Moderator.getFocusComponent(), type: 'set'});
- var sessionId = localStorage.getItem('sessionId');
- if (!sessionId) {
- callback();
- return;
- }
- iq.c('logout', {
- xmlns: 'http://jitsi.org/protocol/focus',
- 'session-id': sessionId
- });
- connection.sendIQ(
- iq,
- function (result) {
- var logoutUrl = $(result).find('logout').attr('logout-url');
- if (logoutUrl) {
- logoutUrl = decodeURIComponent(logoutUrl);
- }
- console.info("Log out OK, url: " + logoutUrl, result);
- localStorage.removeItem('sessionId');
- callback(logoutUrl);
- },
- function (error) {
- console.error("Logout error", error);
- }
- );
- }
-};
-
-module.exports = Moderator;
-
-
-
-
-},{"../../service/authentication/AuthenticationEvents":93,"../../service/xmpp/XMPPEvents":97,"../settings/Settings":38}],54:[function(require,module,exports){
-/* global $, $iq, config, connection, focusMucJid, messageHandler, Moderator,
- Toolbar, Util */
-var Moderator = require("./moderator");
-
-
-var recordingToken = null;
-var recordingEnabled;
-
-/**
- * Whether to use a jirecon component for recording, or use the videobridge
- * through COLIBRI.
- */
-var useJirecon = (typeof config.hosts.jirecon != "undefined");
-
-/**
- * The ID of the jirecon recording session. Jirecon generates it when we
- * initially start recording, and it needs to be used in subsequent requests
- * to jirecon.
- */
-var jireconRid = null;
-
-function setRecordingToken(token) {
- recordingToken = token;
-}
-
-function setRecording(state, token, callback, connection) {
- if (useJirecon){
- setRecordingJirecon(state, token, callback, connection);
- } else {
- setRecordingColibri(state, token, callback, connection);
- }
-}
-
-function setRecordingJirecon(state, token, callback, connection) {
- if (state == recordingEnabled){
- return;
- }
-
- var iq = $iq({to: config.hosts.jirecon, type: 'set'})
- .c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
- action: state ? 'start' : 'stop',
- mucjid: connection.emuc.roomjid});
- if (!state){
- iq.attrs({rid: jireconRid});
- }
-
- console.log('Start recording');
-
- connection.sendIQ(
- iq,
- function (result) {
- // TODO wait for an IQ with the real status, since this is
- // provisional?
- jireconRid = $(result).find('recording').attr('rid');
- console.log('Recording ' + (state ? 'started' : 'stopped') +
- '(jirecon)' + result);
- recordingEnabled = state;
- if (!state){
- jireconRid = null;
- }
-
- callback(state);
- },
- function (error) {
- console.log('Failed to start recording, error: ', error);
- callback(recordingEnabled);
- });
-}
-
-// Sends a COLIBRI message which enables or disables (according to 'state')
-// the recording on the bridge. Waits for the result IQ and calls 'callback'
-// with the new recording state, according to the IQ.
-function setRecordingColibri(state, token, callback, connection) {
- var elem = $iq({to: connection.emuc.focusMucJid, type: 'set'});
- elem.c('conference', {
- xmlns: 'http://jitsi.org/protocol/colibri'
- });
- elem.c('recording', {state: state, token: token});
-
- connection.sendIQ(elem,
- function (result) {
- console.log('Set recording "', state, '". Result:', result);
- var recordingElem = $(result).find('>conference>recording');
- var newState = ('true' === recordingElem.attr('state'));
-
- recordingEnabled = newState;
- callback(newState);
- },
- function (error) {
- console.warn(error);
- callback(recordingEnabled);
- }
- );
-}
-
-var Recording = {
- toggleRecording: function (tokenEmptyCallback,
- startingCallback, startedCallback, connection) {
- if (!Moderator.isModerator()) {
- console.log(
- 'non-focus, or conference not yet organized:' +
- ' not enabling recording');
- return;
- }
-
- var self = this;
- // Jirecon does not (currently) support a token.
- if (!recordingToken && !useJirecon) {
- tokenEmptyCallback(function (value) {
- setRecordingToken(value);
- self.toggleRecording(tokenEmptyCallback,
- startingCallback, startedCallback, connection);
- });
-
- return;
- }
-
- var oldState = recordingEnabled;
- startingCallback(!oldState);
- setRecording(!oldState,
- recordingToken,
- function (state) {
- console.log("New recording state: ", state);
- if (state === oldState) {
- // FIXME: new focus:
- // this will not work when moderator changes
- // during active session. Then it will assume that
- // recording status has changed to true, but it might have
- // been already true(and we only received actual status from
- // the focus).
- //
- // SO we start with status null, so that it is initialized
- // here and will fail only after second click, so if invalid
- // token was used we have to press the button twice before
- // current status will be fetched and token will be reset.
- //
- // Reliable way would be to return authentication error.
- // Or status update when moderator connects.
- // Or we have to stop recording session when current
- // moderator leaves the room.
-
- // Failed to change, reset the token because it might
- // have been wrong
- setRecordingToken(null);
- }
- startedCallback(state);
-
- },
- connection
- );
- }
-
-}
-
+SDPUtil = {
+ iceparams: function (mediadesc, sessiondesc) {
+ var data = null;
+ if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
+ SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
+ data = {
+ ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
+ pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
+ };
+ }
+ return data;
+ },
+ parse_iceufrag: function (line) {
+ return line.substring(12);
+ },
+ build_iceufrag: function (frag) {
+ return 'a=ice-ufrag:' + frag;
+ },
+ parse_icepwd: function (line) {
+ return line.substring(10);
+ },
+ build_icepwd: function (pwd) {
+ return 'a=ice-pwd:' + pwd;
+ },
+ parse_mid: function (line) {
+ return line.substring(6);
+ },
+ parse_mline: function (line) {
+ var parts = line.substring(2).split(' '),
+ data = {};
+ data.media = parts.shift();
+ data.port = parts.shift();
+ data.proto = parts.shift();
+ if (parts[parts.length - 1] === '') { // trailing whitespace
+ parts.pop();
+ }
+ data.fmt = parts;
+ return data;
+ },
+ build_mline: function (mline) {
+ return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
+ },
+ parse_rtpmap: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.id = parts.shift();
+ parts = parts[0].split('/');
+ data.name = parts.shift();
+ data.clockrate = parts.shift();
+ data.channels = parts.length ? parts.shift() : '1';
+ return data;
+ },
+ /**
+ * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
+ * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
+ * @returns [SCTP port number, protocol, streams]
+ */
+ parse_sctpmap: function (line)
+ {
+ var parts = line.substring(10).split(' ');
+ var sctpPort = parts[0];
+ var protocol = parts[1];
+ // Stream count is optional
+ var streamCount = parts.length > 2 ? parts[2] : null;
+ return [sctpPort, protocol, streamCount];// SCTP port
+ },
+ build_rtpmap: function (el) {
+ var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
+ if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
+ line += '/' + el.getAttribute('channels');
+ }
+ return line;
+ },
+ parse_crypto: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.tag = parts.shift();
+ data['crypto-suite'] = parts.shift();
+ data['key-params'] = parts.shift();
+ if (parts.length) {
+ data['session-params'] = parts.join(' ');
+ }
+ return data;
+ },
+ parse_fingerprint: function (line) { // RFC 4572
+ var parts = line.substring(14).split(' '),
+ data = {};
+ data.hash = parts.shift();
+ data.fingerprint = parts.shift();
+ // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
+ return data;
+ },
+ parse_fmtp: function (line) {
+ var parts = line.split(' '),
+ i, key, value,
+ data = [];
+ parts.shift();
+ parts = parts.join(' ').split(';');
+ for (i = 0; i < parts.length; i++) {
+ key = parts[i].split('=')[0];
+ while (key.length && key[0] == ' ') {
+ key = key.substring(1);
+ }
+ value = parts[i].split('=')[1];
+ if (key && value) {
+ data.push({name: key, value: value});
+ } else if (key) {
+ // rfc 4733 (DTMF) style stuff
+ data.push({name: '', value: key});
+ }
+ }
+ return data;
+ },
+ parse_icecandidate: function (line) {
+ var candidate = {},
+ elems = line.split(' ');
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+ candidate.generation = 0; // default value, may be overwritten below
+ for (var i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ build_icecandidate: function (cand) {
+ var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
+ line += ' ';
+ switch (cand.type) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand['rel-addr'];
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand['rel-port'];
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.hasOwnAttribute('tcptype')) {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.tcptype;
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
+ return line;
+ },
+ parse_ssrc: function (desc) {
+ // proprietary mapping of a=ssrc lines
+ // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
+ // and parse according to that
+ var lines = desc.split('\r\n'),
+ data = {};
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, 7) == 'a=ssrc:') {
+ var idx = lines[i].indexOf(' ');
+ data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
+ }
+ }
+ return data;
+ },
+ parse_rtcpfb: function (line) {
+ var parts = line.substr(10).split(' ');
+ var data = {};
+ data.pt = parts.shift();
+ data.type = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ parse_extmap: function (line) {
+ var parts = line.substr(9).split(' ');
+ var data = {};
+ data.value = parts.shift();
+ if (data.value.indexOf('/') != -1) {
+ data.direction = data.value.substr(data.value.indexOf('/') + 1);
+ data.value = data.value.substr(0, data.value.indexOf('/'));
+ } else {
+ data.direction = 'both';
+ }
+ data.uri = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ find_line: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n');
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle) {
+ return lines[i];
+ }
+ }
+ if (!sessionpart) {
+ return false;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ return lines[j];
+ }
+ }
+ return false;
+ },
+ find_lines: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n'),
+ needles = [];
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle)
+ needles.push(lines[i]);
+ }
+ if (needles.length || !sessionpart) {
+ return needles;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ needles.push(lines[j]);
+ }
+ }
+ return needles;
+ },
+ candidateToJingle: function (line) {
+ // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
+ //
+ if (line.indexOf('candidate:') === 0) {
+ line = 'a=' + line;
+ } else if (line.substring(0, 12) != 'a=candidate:') {
+ console.log('parseCandidate called with a line that is not a candidate line');
+ console.log(line);
+ return null;
+ }
+ if (line.substring(line.length - 2) == '\r\n') // chomp it
+ line = line.substring(0, line.length - 2);
+ var candidate = {},
+ elems = line.split(' '),
+ i;
+ if (elems[6] != 'typ') {
+ console.log('did not find typ in the right place');
+ console.log(line);
+ return null;
+ }
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+
+ candidate.generation = '0'; // default, may be overwritten below
+ for (i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ candidateFromJingle: function (cand) {
+ var line = 'a=candidate:';
+ line += cand.getAttribute('foundation');
+ line += ' ';
+ line += cand.getAttribute('component');
+ line += ' ';
+ line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
+ line += ' ';
+ line += cand.getAttribute('priority');
+ line += ' ';
+ line += cand.getAttribute('ip');
+ line += ' ';
+ line += cand.getAttribute('port');
+ line += ' ';
+ line += 'typ';
+ line += ' ' + cand.getAttribute('type');
+ line += ' ';
+ switch (cand.getAttribute('type')) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand.getAttribute('rel-addr');
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand.getAttribute('rel-port');
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.getAttribute('tcptype');
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.getAttribute('generation') || '0';
+ return line + '\r\n';
+ }
+};
+module.exports = SDPUtil;
+},{}],53:[function(require,module,exports){
+function TraceablePeerConnection(ice_config, constraints) {
+ var self = this;
+ var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
+ this.peerconnection = new RTCPeerconnection(ice_config, constraints);
+ this.updateLog = [];
+ this.stats = {};
+ this.statsinterval = null;
+ this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
+ var Interop = require('sdp-interop').Interop;
+ this.interop = new Interop();
+
+ // override as desired
+ this.trace = function (what, info) {
+ //console.warn('WTRACE', what, info);
+ self.updateLog.push({
+ time: new Date(),
+ type: what,
+ value: info || ""
+ });
+ };
+ this.onicecandidate = null;
+ this.peerconnection.onicecandidate = function (event) {
+ self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
+ if (self.onicecandidate !== null) {
+ self.onicecandidate(event);
+ }
+ };
+ this.onaddstream = null;
+ this.peerconnection.onaddstream = function (event) {
+ self.trace('onaddstream', event.stream.id);
+ if (self.onaddstream !== null) {
+ self.onaddstream(event);
+ }
+ };
+ this.onremovestream = null;
+ this.peerconnection.onremovestream = function (event) {
+ self.trace('onremovestream', event.stream.id);
+ if (self.onremovestream !== null) {
+ self.onremovestream(event);
+ }
+ };
+ this.onsignalingstatechange = null;
+ this.peerconnection.onsignalingstatechange = function (event) {
+ self.trace('onsignalingstatechange', self.signalingState);
+ if (self.onsignalingstatechange !== null) {
+ self.onsignalingstatechange(event);
+ }
+ };
+ this.oniceconnectionstatechange = null;
+ this.peerconnection.oniceconnectionstatechange = function (event) {
+ self.trace('oniceconnectionstatechange', self.iceConnectionState);
+ if (self.oniceconnectionstatechange !== null) {
+ self.oniceconnectionstatechange(event);
+ }
+ };
+ this.onnegotiationneeded = null;
+ this.peerconnection.onnegotiationneeded = function (event) {
+ self.trace('onnegotiationneeded');
+ if (self.onnegotiationneeded !== null) {
+ self.onnegotiationneeded(event);
+ }
+ };
+ self.ondatachannel = null;
+ this.peerconnection.ondatachannel = function (event) {
+ self.trace('ondatachannel', event);
+ if (self.ondatachannel !== null) {
+ self.ondatachannel(event);
+ }
+ };
+ if (!navigator.mozGetUserMedia && this.maxstats) {
+ this.statsinterval = window.setInterval(function() {
+ self.peerconnection.getStats(function(stats) {
+ var results = stats.result();
+ for (var i = 0; i < results.length; ++i) {
+ //console.log(results[i].type, results[i].id, results[i].names())
+ var now = new Date();
+ results[i].names().forEach(function (name) {
+ var id = results[i].id + '-' + name;
+ if (!self.stats[id]) {
+ self.stats[id] = {
+ startTime: now,
+ endTime: now,
+ values: [],
+ times: []
+ };
+ }
+ self.stats[id].values.push(results[i].stat(name));
+ self.stats[id].times.push(now.getTime());
+ if (self.stats[id].values.length > self.maxstats) {
+ self.stats[id].values.shift();
+ self.stats[id].times.shift();
+ }
+ self.stats[id].endTime = now;
+ });
+ }
+ });
+
+ }, 1000);
+ }
+};
+
+dumpSDP = function(description) {
+ if (typeof description === 'undefined' || description == null) {
+ return '';
+ }
+
+ return 'type: ' + description.type + '\r\n' + description.sdp;
+};
+
+if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
+ TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
+ TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
+ TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
+ this.trace('getLocalDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.localDescription));
+ // if we're running on FF, transform to Plan B first.
+ var desc = this.peerconnection.localDescription;
+ if (navigator.mozGetUserMedia) {
+ desc = this.interop.toPlanB(desc);
+ } else {
+ desc = APP.simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription);
+ }
+ this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
+ return desc;
+ });
+ TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() {
+ this.trace('getRemoteDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.remoteDescription));
+ // if we're running on FF, transform to Plan B first.
+ var desc = this.peerconnection.remoteDescription;
+ if (navigator.mozGetUserMedia) {
+ desc = this.interop.toPlanB(desc);
+ } else {
+ desc = APP.simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription);
+ }
+ this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
+ return desc;
+ });
+}
+
+TraceablePeerConnection.prototype.addStream = function (stream) {
+ this.trace('addStream', stream.id);
+ APP.simulcast.resetSender();
+ try
+ {
+ this.peerconnection.addStream(stream);
+ }
+ catch (e)
+ {
+ console.error(e);
+ return;
+ }
+};
+
+TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
+ this.trace('removeStream', stream.id);
+ APP.simulcast.resetSender();
+ if(stopStreams) {
+ stream.getAudioTracks().forEach(function (track) {
+ track.stop();
+ });
+ stream.getVideoTracks().forEach(function (track) {
+ track.stop();
+ });
+ }
+
+ try {
+ // FF doesn't support this yet.
+ this.peerconnection.removeStream(stream);
+ } catch (e) {
+ console.error(e);
+ }
+};
+
+TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
+ this.trace('createDataChannel', label, opts);
+ return this.peerconnection.createDataChannel(label, opts);
+};
+
+TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
+ this.trace('setLocalDescription::preTransform (Plan B)', dumpSDP(description));
+ // if we're running on FF, transform to Plan A first.
+ if (navigator.mozGetUserMedia) {
+ description = this.interop.toPlanA(description);
+ } else {
+ description = APP.simulcast.transformLocalDescription(description);
+ }
+ this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
+ var self = this;
+ this.peerconnection.setLocalDescription(description,
+ function () {
+ self.trace('setLocalDescriptionOnSuccess');
+ successCallback();
+ },
+ function (err) {
+ self.trace('setLocalDescriptionOnFailure', err);
+ failureCallback(err);
+ }
+ );
+ /*
+ if (this.statsinterval === null && this.maxstats > 0) {
+ // start gathering stats
+ }
+ */
+};
+
+TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
+ this.trace('setRemoteDescription::preTransform (Plan B)', dumpSDP(description));
+ // if we're running on FF, transform to Plan A first.
+ if (navigator.mozGetUserMedia) {
+ description = this.interop.toPlanA(description);
+ }
+ else {
+ description = APP.simulcast.transformRemoteDescription(description);
+ }
+ this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
+ var self = this;
+ this.peerconnection.setRemoteDescription(description,
+ function () {
+ self.trace('setRemoteDescriptionOnSuccess');
+ successCallback();
+ },
+ function (err) {
+ self.trace('setRemoteDescriptionOnFailure', err);
+ failureCallback(err);
+ }
+ );
+ /*
+ if (this.statsinterval === null && this.maxstats > 0) {
+ // start gathering stats
+ }
+ */
+};
+
+TraceablePeerConnection.prototype.close = function () {
+ this.trace('stop');
+ if (this.statsinterval !== null) {
+ window.clearInterval(this.statsinterval);
+ this.statsinterval = null;
+ }
+ this.peerconnection.close();
+};
+
+TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
+ var self = this;
+ this.trace('createOffer', JSON.stringify(constraints, null, ' '));
+ this.peerconnection.createOffer(
+ function (offer) {
+ self.trace('createOfferOnSuccess::preTransform (Plan A)', dumpSDP(offer));
+ // if we're running on FF, transform to Plan B first.
+ if (navigator.mozGetUserMedia) {
+ offer = self.interop.toPlanB(offer);
+ }
+ self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
+ successCallback(offer);
+ },
+ function(err) {
+ self.trace('createOfferOnFailure', err);
+ failureCallback(err);
+ },
+ constraints
+ );
+};
+
+TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
+ var self = this;
+ this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
+ this.peerconnection.createAnswer(
+ function (answer) {
+ self.trace('createAnswerOnSuccess::preTransfom (Plan A)', dumpSDP(answer));
+ // if we're running on FF, transform to Plan A first.
+ if (navigator.mozGetUserMedia) {
+ answer = self.interop.toPlanB(answer);
+ } else {
+ answer = APP.simulcast.transformAnswer(answer);
+ }
+ self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer));
+ successCallback(answer);
+ },
+ function(err) {
+ self.trace('createAnswerOnFailure', err);
+ failureCallback(err);
+ },
+ constraints
+ );
+};
+
+TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
+ var self = this;
+ this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
+ this.peerconnection.addIceCandidate(candidate);
+ /* maybe later
+ this.peerconnection.addIceCandidate(candidate,
+ function () {
+ self.trace('addIceCandidateOnSuccess');
+ successCallback();
+ },
+ function (err) {
+ self.trace('addIceCandidateOnFailure', err);
+ failureCallback(err);
+ }
+ );
+ */
+};
+
+TraceablePeerConnection.prototype.getStats = function(callback, errback) {
+ if (navigator.mozGetUserMedia) {
+ // ignore for now...
+ if(!errback)
+ errback = function () {
+
+ }
+ this.peerconnection.getStats(null,callback,errback);
+ } else {
+ this.peerconnection.getStats(callback);
+ }
+};
+
+module.exports = TraceablePeerConnection;
+
+
+},{"sdp-interop":81}],54:[function(require,module,exports){
+/* global $, $iq, APP, config, connection, UI, messageHandler,
+ roomName, sessionTerminated, Strophe, Util */
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var Settings = require("../settings/Settings");
+
+var AuthenticationEvents
+ = require("../../service/authentication/AuthenticationEvents");
+
+/**
+ * Contains logic responsible for enabling/disabling functionality available
+ * only to moderator users.
+ */
+var connection = null;
+var focusUserJid;
+
+function createExpBackoffTimer(step) {
+ var count = 1;
+ return function (reset) {
+ // Reset call
+ if (reset) {
+ count = 1;
+ return;
+ }
+ // Calculate next timeout
+ var timeout = Math.pow(2, count - 1);
+ count += 1;
+ return timeout * step;
+ };
+}
+
+var getNextTimeout = createExpBackoffTimer(1000);
+var getNextErrorTimeout = createExpBackoffTimer(1000);
+// External authentication stuff
+var externalAuthEnabled = false;
+// Sip gateway can be enabled by configuring Jigasi host in config.js or
+// it will be enabled automatically if focus detects the component through
+// service discovery.
+var sipGatewayEnabled = config.hosts.call_control !== undefined;
+
+var eventEmitter = null;
+
+var Moderator = {
+ isModerator: function () {
+ return connection && connection.emuc.isModerator();
+ },
+
+ isPeerModerator: function (peerJid) {
+ return connection &&
+ connection.emuc.getMemberRole(peerJid) === 'moderator';
+ },
+
+ isExternalAuthEnabled: function () {
+ return externalAuthEnabled;
+ },
+
+ isSipGatewayEnabled: function () {
+ return sipGatewayEnabled;
+ },
+
+ setConnection: function (con) {
+ connection = con;
+ },
+
+ init: function (xmpp, emitter) {
+ this.xmppService = xmpp;
+ eventEmitter = emitter;
+
+ // Message listener that talks to POPUP window
+ function listener(event) {
+ if (event.data && event.data.sessionId) {
+ if (event.origin !== window.location.origin) {
+ console.warn(
+ "Ignoring sessionId from different origin: " + event.origin);
+ return;
+ }
+ localStorage.setItem('sessionId', event.data.sessionId);
+ // After popup is closed we will authenticate
+ }
+ }
+ // Register
+ if (window.addEventListener) {
+ window.addEventListener("message", listener, false);
+ } else {
+ window.attachEvent("onmessage", listener);
+ }
+ },
+
+ onMucLeft: function (jid) {
+ console.info("Someone left is it focus ? " + jid);
+ var resource = Strophe.getResourceFromJid(jid);
+ if (resource === 'focus' && !this.xmppService.sessionTerminated) {
+ console.info(
+ "Focus has left the room - leaving conference");
+ //hangUp();
+ // We'd rather reload to have everything re-initialized
+ // FIXME: show some message before reload
+ location.reload();
+ }
+ },
+
+ setFocusUserJid: function (focusJid) {
+ if (!focusUserJid) {
+ focusUserJid = focusJid;
+ console.info("Focus jid set to: " + focusUserJid);
+ }
+ },
+
+ getFocusUserJid: function () {
+ return focusUserJid;
+ },
+
+ getFocusComponent: function () {
+ // Get focus component address
+ var focusComponent = config.hosts.focus;
+ // If not specified use default: 'focus.domain'
+ if (!focusComponent) {
+ focusComponent = 'focus.' + config.hosts.domain;
+ }
+ return focusComponent;
+ },
+
+ createConferenceIq: function (roomName) {
+ // Generate create conference IQ
+ var elem = $iq({to: Moderator.getFocusComponent(), type: 'set'});
+
+ // Session Id used for authentication
+ var sessionId = localStorage.getItem('sessionId');
+ var machineUID = Settings.getSettings().uid;
+
+ console.info(
+ "Session ID: " + sessionId + " machine UID: " + machineUID);
+
+ elem.c('conference', {
+ xmlns: 'http://jitsi.org/protocol/focus',
+ room: roomName,
+ 'machine-uid': machineUID
+ });
+
+ if (sessionId) {
+ elem.attrs({ 'session-id': sessionId});
+ }
+
+ if (config.hosts.bridge !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'bridge', value: config.hosts.bridge})
+ .up();
+ }
+ // Tell the focus we have Jigasi configured
+ if (config.hosts.call_control !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'call_control', value: config.hosts.call_control})
+ .up();
+ }
+ if (config.channelLastN !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'channelLastN', value: config.channelLastN})
+ .up();
+ }
+ if (config.adaptiveLastN !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'adaptiveLastN', value: config.adaptiveLastN})
+ .up();
+ }
+ if (config.adaptiveSimulcast !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
+ .up();
+ }
+ if (config.openSctp !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'openSctp', value: config.openSctp})
+ .up();
+ }
+ if (config.enableFirefoxSupport !== undefined) {
+ elem.c(
+ 'property',
+ { name: 'enableFirefoxHacks',
+ value: config.enableFirefoxSupport})
+ .up();
+ }
+ elem.up();
+ return elem;
+ },
+
+ parseSessionId: function (resultIq) {
+ var sessionId = $(resultIq).find('conference').attr('session-id');
+ if (sessionId) {
+ console.info('Received sessionId: ' + sessionId);
+ localStorage.setItem('sessionId', sessionId);
+ }
+ },
+
+ parseConfigOptions: function (resultIq) {
+
+ Moderator.setFocusUserJid(
+ $(resultIq).find('conference').attr('focusjid'));
+
+ var authenticationEnabled
+ = $(resultIq).find(
+ '>conference>property' +
+ '[name=\'authentication\'][value=\'true\']').length > 0;
+
+ console.info("Authentication enabled: " + authenticationEnabled);
+
+ externalAuthEnabled
+ = $(resultIq).find(
+ '>conference>property' +
+ '[name=\'externalAuth\'][value=\'true\']').length > 0;
+
+ console.info('External authentication enabled: ' + externalAuthEnabled);
+
+ if (!externalAuthEnabled) {
+ // We expect to receive sessionId in 'internal' authentication mode
+ Moderator.parseSessionId(resultIq);
+ }
+
+ var authIdentity = $(resultIq).find('>conference').attr('identity');
+
+ eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
+ authenticationEnabled, authIdentity);
+
+ // Check if focus has auto-detected Jigasi component(this will be also
+ // included if we have passed our host from the config)
+ if ($(resultIq).find(
+ '>conference>property' +
+ '[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
+ sipGatewayEnabled = true;
+ }
+
+ console.info("Sip gateway enabled: " + sipGatewayEnabled);
+ },
+
+ // FIXME: we need to show the fact that we're waiting for the focus
+ // to the user(or that focus is not available)
+ allocateConferenceFocus: function (roomName, callback) {
+ // Try to use focus user JID from the config
+ Moderator.setFocusUserJid(config.focusUserJid);
+ // Send create conference IQ
+ var iq = Moderator.createConferenceIq(roomName);
+ var self = this;
+ connection.sendIQ(
+ iq,
+ function (result) {
+
+ // Setup config options
+ Moderator.parseConfigOptions(result);
+
+ if ('true' === $(result).find('conference').attr('ready')) {
+ // Reset both timers
+ getNextTimeout(true);
+ getNextErrorTimeout(true);
+ // Exec callback
+ callback();
+ } else {
+ var waitMs = getNextTimeout();
+ console.info("Waiting for the focus... " + waitMs);
+ // Reset error timeout
+ getNextErrorTimeout(true);
+ window.setTimeout(
+ function () {
+ Moderator.allocateConferenceFocus(
+ roomName, callback);
+ }, waitMs);
+ }
+ },
+ function (error) {
+ // Invalid session ? remove and try again
+ // without session ID to get a new one
+ var invalidSession
+ = $(error).find('>error>session-invalid').length;
+ if (invalidSession) {
+ console.info("Session expired! - removing");
+ localStorage.removeItem("sessionId");
+ }
+ if ($(error).find('>error>graceful-shutdown').length) {
+ eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
+ return;
+ }
+ // Check for error returned by the reservation system
+ var reservationErr = $(error).find('>error>reservation-error');
+ if (reservationErr.length) {
+ // Trigger error event
+ var errorCode = reservationErr.attr('error-code');
+ var errorMsg;
+ if ($(error).find('>error>text')) {
+ errorMsg = $(error).find('>error>text').text();
+ }
+ eventEmitter.emit(
+ XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
+ return;
+ }
+ // Not authorized to create new room
+ if ($(error).find('>error>not-authorized').length) {
+ console.warn("Unauthorized to start the conference", error);
+ var toDomain
+ = Strophe.getDomainFromJid(error.getAttribute('to'));
+ if (toDomain !== config.hosts.anonymousdomain) {
+ // FIXME: "is external" should come either from
+ // the focus or config.js
+ externalAuthEnabled = true;
+ }
+ eventEmitter.emit(
+ XMPPEvents.AUTHENTICATION_REQUIRED,
+ function () {
+ Moderator.allocateConferenceFocus(
+ roomName, callback);
+ });
+ return;
+ }
+ var waitMs = getNextErrorTimeout();
+ console.error("Focus error, retry after " + waitMs, error);
+ // Show message
+ var focusComponent = Moderator.getFocusComponent();
+ var retrySec = waitMs / 1000;
+ // FIXME: message is duplicated ?
+ // Do not show in case of session invalid
+ // which means just a retry
+ if (!invalidSession) {
+ APP.UI.messageHandler.notify(
+ null, "notify.focus",
+ 'disconnected', "notify.focusFail",
+ {component: focusComponent, ms: retrySec});
+ }
+ // Reset response timeout
+ getNextTimeout(true);
+ window.setTimeout(
+ function () {
+ Moderator.allocateConferenceFocus(roomName, callback);
+ }, waitMs);
+ }
+ );
+ },
+
+ getLoginUrl: function (roomName, urlCallback) {
+ var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
+ iq.c('login-url', {
+ xmlns: 'http://jitsi.org/protocol/focus',
+ room: roomName,
+ 'machine-uid': Settings.getSettings().uid
+ });
+ connection.sendIQ(
+ iq,
+ function (result) {
+ var url = $(result).find('login-url').attr('url');
+ url = url = decodeURIComponent(url);
+ if (url) {
+ console.info("Got auth url: " + url);
+ urlCallback(url);
+ } else {
+ console.error(
+ "Failed to get auth url from the focus", result);
+ }
+ },
+ function (error) {
+ console.error("Get auth url error", error);
+ }
+ );
+ },
+ getPopupLoginUrl: function (roomName, urlCallback) {
+ var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
+ iq.c('login-url', {
+ xmlns: 'http://jitsi.org/protocol/focus',
+ room: roomName,
+ 'machine-uid': Settings.getSettings().uid,
+ popup: true
+ });
+ connection.sendIQ(
+ iq,
+ function (result) {
+ var url = $(result).find('login-url').attr('url');
+ url = url = decodeURIComponent(url);
+ if (url) {
+ console.info("Got POPUP auth url: " + url);
+ urlCallback(url);
+ } else {
+ console.error(
+ "Failed to get POPUP auth url from the focus", result);
+ }
+ },
+ function (error) {
+ console.error('Get POPUP auth url error', error);
+ }
+ );
+ },
+ logout: function (callback) {
+ var iq = $iq({to: Moderator.getFocusComponent(), type: 'set'});
+ var sessionId = localStorage.getItem('sessionId');
+ if (!sessionId) {
+ callback();
+ return;
+ }
+ iq.c('logout', {
+ xmlns: 'http://jitsi.org/protocol/focus',
+ 'session-id': sessionId
+ });
+ connection.sendIQ(
+ iq,
+ function (result) {
+ var logoutUrl = $(result).find('logout').attr('logout-url');
+ if (logoutUrl) {
+ logoutUrl = decodeURIComponent(logoutUrl);
+ }
+ console.info("Log out OK, url: " + logoutUrl, result);
+ localStorage.removeItem('sessionId');
+ callback(logoutUrl);
+ },
+ function (error) {
+ console.error("Logout error", error);
+ }
+ );
+ }
+};
+
+module.exports = Moderator;
+
+
+
+
+},{"../../service/authentication/AuthenticationEvents":94,"../../service/xmpp/XMPPEvents":98,"../settings/Settings":39}],55:[function(require,module,exports){
+/* global $, $iq, config, connection, focusMucJid, messageHandler, Moderator,
+ Toolbar, Util */
+var Moderator = require("./moderator");
+
+
+var recordingToken = null;
+var recordingEnabled;
+
+/**
+ * Whether to use a jirecon component for recording, or use the videobridge
+ * through COLIBRI.
+ */
+var useJirecon = (typeof config.hosts.jirecon != "undefined");
+
+/**
+ * The ID of the jirecon recording session. Jirecon generates it when we
+ * initially start recording, and it needs to be used in subsequent requests
+ * to jirecon.
+ */
+var jireconRid = null;
+
+function setRecordingToken(token) {
+ recordingToken = token;
+}
+
+function setRecording(state, token, callback, connection) {
+ if (useJirecon){
+ setRecordingJirecon(state, token, callback, connection);
+ } else {
+ setRecordingColibri(state, token, callback, connection);
+ }
+}
+
+function setRecordingJirecon(state, token, callback, connection) {
+ if (state == recordingEnabled){
+ return;
+ }
+
+ var iq = $iq({to: config.hosts.jirecon, type: 'set'})
+ .c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
+ action: state ? 'start' : 'stop',
+ mucjid: connection.emuc.roomjid});
+ if (!state){
+ iq.attrs({rid: jireconRid});
+ }
+
+ console.log('Start recording');
+
+ connection.sendIQ(
+ iq,
+ function (result) {
+ // TODO wait for an IQ with the real status, since this is
+ // provisional?
+ jireconRid = $(result).find('recording').attr('rid');
+ console.log('Recording ' + (state ? 'started' : 'stopped') +
+ '(jirecon)' + result);
+ recordingEnabled = state;
+ if (!state){
+ jireconRid = null;
+ }
+
+ callback(state);
+ },
+ function (error) {
+ console.log('Failed to start recording, error: ', error);
+ callback(recordingEnabled);
+ });
+}
+
+// Sends a COLIBRI message which enables or disables (according to 'state')
+// the recording on the bridge. Waits for the result IQ and calls 'callback'
+// with the new recording state, according to the IQ.
+function setRecordingColibri(state, token, callback, connection) {
+ var elem = $iq({to: connection.emuc.focusMucJid, type: 'set'});
+ elem.c('conference', {
+ xmlns: 'http://jitsi.org/protocol/colibri'
+ });
+ elem.c('recording', {state: state, token: token});
+
+ connection.sendIQ(elem,
+ function (result) {
+ console.log('Set recording "', state, '". Result:', result);
+ var recordingElem = $(result).find('>conference>recording');
+ var newState = ('true' === recordingElem.attr('state'));
+
+ recordingEnabled = newState;
+ callback(newState);
+ },
+ function (error) {
+ console.warn(error);
+ callback(recordingEnabled);
+ }
+ );
+}
+
+var Recording = {
+ toggleRecording: function (tokenEmptyCallback,
+ startingCallback, startedCallback, connection) {
+ if (!Moderator.isModerator()) {
+ console.log(
+ 'non-focus, or conference not yet organized:' +
+ ' not enabling recording');
+ return;
+ }
+
+ var self = this;
+ // Jirecon does not (currently) support a token.
+ if (!recordingToken && !useJirecon) {
+ tokenEmptyCallback(function (value) {
+ setRecordingToken(value);
+ self.toggleRecording(tokenEmptyCallback,
+ startingCallback, startedCallback, connection);
+ });
+
+ return;
+ }
+
+ var oldState = recordingEnabled;
+ startingCallback(!oldState);
+ setRecording(!oldState,
+ recordingToken,
+ function (state) {
+ console.log("New recording state: ", state);
+ if (state === oldState) {
+ // FIXME: new focus:
+ // this will not work when moderator changes
+ // during active session. Then it will assume that
+ // recording status has changed to true, but it might have
+ // been already true(and we only received actual status from
+ // the focus).
+ //
+ // SO we start with status null, so that it is initialized
+ // here and will fail only after second click, so if invalid
+ // token was used we have to press the button twice before
+ // current status will be fetched and token will be reset.
+ //
+ // Reliable way would be to return authentication error.
+ // Or status update when moderator connects.
+ // Or we have to stop recording session when current
+ // moderator leaves the room.
+
+ // Failed to change, reset the token because it might
+ // have been wrong
+ setRecordingToken(null);
+ }
+ startedCallback(state);
+
+ },
+ connection
+ );
+ }
+
+}
+
module.exports = Recording;
-},{"./moderator":53}],55:[function(require,module,exports){
-/* jshint -W117 */
-/* a simple MUC connection plugin
- * can only handle a single MUC room
- */
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var Moderator = require("./moderator");
-var JingleSession = require("./JingleSession");
-
-var bridgeIsDown = false;
-
-module.exports = function(XMPP, eventEmitter) {
- Strophe.addConnectionPlugin('emuc', {
- connection: null,
- roomjid: null,
- myroomjid: null,
- members: {},
- list_members: [], // so we can elect a new focus
- presMap: {},
- preziMap: {},
- joined: false,
- isOwner: false,
- role: null,
- focusMucJid: null,
- ssrc2jid: {},
- init: function (conn) {
- this.connection = conn;
- },
- initPresenceMap: function (myroomjid) {
- this.presMap['to'] = myroomjid;
- this.presMap['xns'] = 'http://jabber.org/protocol/muc';
- },
- doJoin: function (jid, password) {
- this.myroomjid = jid;
-
- console.info("Joined MUC as " + this.myroomjid);
-
- this.initPresenceMap(this.myroomjid);
-
- if (!this.roomjid) {
- this.roomjid = Strophe.getBareJidFromJid(jid);
- // add handlers (just once)
- this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
- this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
- this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
- this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
- }
- if (password !== undefined) {
- this.presMap['password'] = password;
- }
- this.sendPresence();
- },
- doLeave: function () {
- console.log("do leave", this.myroomjid);
- var pres = $pres({to: this.myroomjid, type: 'unavailable' });
- this.presMap.length = 0;
- this.connection.send(pres);
- },
- createNonAnonymousRoom: function () {
- // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
-
- var getForm = $iq({type: 'get', to: this.roomjid})
- .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
- .c('x', {xmlns: 'jabber:x:data', type: 'submit'});
-
- var self = this;
-
- this.connection.sendIQ(getForm, function (form) {
-
- if (!$(form).find(
- '>query>x[xmlns="jabber:x:data"]' +
- '>field[var="muc#roomconfig_whois"]').length) {
-
- console.error('non-anonymous rooms not supported');
- return;
- }
-
- var formSubmit = $iq({to: this.roomjid, type: 'set'})
- .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
-
- formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
-
- formSubmit.c('field', {'var': 'FORM_TYPE'})
- .c('value')
- .t('http://jabber.org/protocol/muc#roomconfig').up().up();
-
- formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
- .c('value').t('anyone').up().up();
-
- self.connection.sendIQ(formSubmit);
-
- }, function (error) {
- console.error("Error getting room configuration form");
- });
- },
- onPresence: function (pres) {
- var from = pres.getAttribute('from');
-
- // What is this for? A workaround for something?
- if (pres.getAttribute('type')) {
- return true;
- }
-
- // Parse etherpad tag.
- var etherpad = $(pres).find('>etherpad');
- if (etherpad.length) {
- if (config.etherpad_base && !Moderator.isModerator()) {
- eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
- }
- }
-
- // Parse prezi tag.
- var presentation = $(pres).find('>prezi');
- if (presentation.length) {
- var url = presentation.attr('url');
- var current = presentation.find('>current').text();
-
- console.log('presentation info received from', from, url);
-
- if (this.preziMap[from] == null) {
- this.preziMap[from] = url;
-
- $(document).trigger('presentationadded.muc', [from, url, current]);
- }
- else {
- $(document).trigger('gotoslide.muc', [from, url, current]);
- }
- }
- else if (this.preziMap[from] != null) {
- var url = this.preziMap[from];
- delete this.preziMap[from];
- $(document).trigger('presentationremoved.muc', [from, url]);
- }
-
- // Parse audio info tag.
- var audioMuted = $(pres).find('>audiomuted');
- if (audioMuted.length) {
- $(document).trigger('audiomuted.muc', [from, audioMuted.text()]);
- }
-
- // Parse video info tag.
- var videoMuted = $(pres).find('>videomuted');
- if (videoMuted.length) {
- $(document).trigger('videomuted.muc', [from, videoMuted.text()]);
- }
-
- var stats = $(pres).find('>stats');
- if (stats.length) {
- var statsObj = {};
- Strophe.forEachChild(stats[0], "stat", function (el) {
- statsObj[el.getAttribute("name")] = el.getAttribute("value");
- });
- eventEmitter.emit(XMPPEvents.REMOTE_STATS, from, statsObj);
- }
-
- // Parse status.
- if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
- this.isOwner = true;
- this.createNonAnonymousRoom();
- }
-
- // Parse roles.
- var member = {};
- member.show = $(pres).find('>show').text();
- member.status = $(pres).find('>status').text();
- var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
- member.affiliation = tmp.attr('affiliation');
- member.role = tmp.attr('role');
-
- // Focus recognition
- member.jid = tmp.attr('jid');
- member.isFocus = false;
- if (member.jid
- && member.jid.indexOf(Moderator.getFocusUserJid() + "/") == 0) {
- member.isFocus = true;
- }
-
- var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
- member.displayName = (nicktag.length > 0 ? nicktag.html() : null);
-
- if (from == this.myroomjid) {
- if (member.affiliation == 'owner') this.isOwner = true;
- if (this.role !== member.role) {
- this.role = member.role;
-
- eventEmitter.emit(XMPPEvents.LOCALROLE_CHANGED,
- from, member, pres, Moderator.isModerator());
- }
- if (!this.joined) {
- this.joined = true;
- eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
- this.list_members.push(from);
- }
- } else if (this.members[from] === undefined) {
- // new participant
- this.members[from] = member;
- this.list_members.push(from);
- console.log('entered', from, member);
- if (member.isFocus) {
- this.focusMucJid = from;
- console.info("Ignore focus: " + from + ", real JID: " + member.jid);
- }
- else {
- var id = $(pres).find('>userID').text();
- var email = $(pres).find('>email');
- if (email.length > 0) {
- id = email.text();
- }
- eventEmitter.emit(XMPPEvents.MUC_ENTER, from, id, member.displayName);
- }
- } else {
- // Presence update for existing participant
- // Watch role change:
- if (this.members[from].role != member.role) {
- this.members[from].role = member.role;
- eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
- member.role, member.displayName);
- }
- }
-
- // Always trigger presence to update bindings
- this.parsePresence(from, member, pres);
-
- // Trigger status message update
- if (member.status) {
- eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
- }
-
- return true;
- },
- onPresenceUnavailable: function (pres) {
- var from = pres.getAttribute('from');
- // room destroyed ?
- if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
- '>destroy').length) {
- var reason;
- var reasonSelect = $(pres).find(
- '>x[xmlns="http://jabber.org/protocol/muc#user"]' +
- '>destroy>reason');
- if (reasonSelect.length) {
- reason = reasonSelect.text();
- }
- XMPP.disposeConference(false);
- eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
- return true;
- }
- // Status code 110 indicates that this notification is "self-presence".
- if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
- delete this.members[from];
- this.list_members.splice(this.list_members.indexOf(from), 1);
- this.onParticipantLeft(from);
- }
- // If the status code is 110 this means we're leaving and we would like
- // to remove everyone else from our view, so we trigger the event.
- else if (this.list_members.length > 1) {
- for (var i = 0; i < this.list_members.length; i++) {
- var member = this.list_members[i];
- delete this.members[i];
- this.list_members.splice(i, 1);
- this.onParticipantLeft(member);
- }
- }
- if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
- $(document).trigger('kicked.muc', [from]);
- if (this.myroomjid === from) {
- XMPP.disposeConference(false);
- eventEmitter.emit(XMPPEvents.KICKED);
- }
- }
- return true;
- },
- onPresenceError: function (pres) {
- var from = pres.getAttribute('from');
- if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
- console.log('on password required', from);
- var self = this;
- eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED, function (value) {
- self.doJoin(from, value);
- });
- } else if ($(pres).find(
- '>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
- var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
- if (toDomain === config.hosts.anonymousdomain) {
- // enter the room by replying with 'not-authorized'. This would
- // result in reconnection from authorized domain.
- // We're either missing Jicofo/Prosody config for anonymous
- // domains or something is wrong.
-// XMPP.promptLogin();
- APP.UI.messageHandler.openReportDialog(null,
- "dialog.joinError", pres);
- } else {
- console.warn('onPresError ', pres);
- APP.UI.messageHandler.openReportDialog(null,
- "dialog.connectError",
- pres);
- }
- } else {
- console.warn('onPresError ', pres);
- APP.UI.messageHandler.openReportDialog(null,
- "dialog.connectError",
- pres);
- }
- return true;
- },
- sendMessage: function (body, nickname) {
- var msg = $msg({to: this.roomjid, type: 'groupchat'});
- msg.c('body', body).up();
- if (nickname) {
- msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
- }
- this.connection.send(msg);
- eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
- },
- setSubject: function (subject) {
- var msg = $msg({to: this.roomjid, type: 'groupchat'});
- msg.c('subject', subject);
- this.connection.send(msg);
- console.log("topic changed to " + subject);
- },
- onMessage: function (msg) {
- // FIXME: this is a hack. but jingle on muc makes nickchanges hard
- var from = msg.getAttribute('from');
- var nick =
- $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
- .text() ||
- Strophe.getResourceFromJid(from);
-
- var txt = $(msg).find('>body').text();
- var type = msg.getAttribute("type");
- if (type == "error") {
- eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
- $(msg).find('>text').text(), txt);
- return true;
- }
-
- var subject = $(msg).find('>subject');
- if (subject.length) {
- var subjectText = subject.text();
- if (subjectText || subjectText == "") {
- eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
- console.log("Subject is changed to " + subjectText);
- }
- }
-
-
- if (txt) {
- console.log('chat', nick, txt);
- eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
- from, nick, txt, this.myroomjid);
- }
- return true;
- },
- lockRoom: function (key, onSuccess, onError, onNotSupported) {
- //http://xmpp.org/extensions/xep-0045.html#roomconfig
- var ob = this;
- this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
- function (res) {
- if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
- var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
- formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
- formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
- formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
- // Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
- formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
- // FIXME: is muc#roomconfig_passwordprotectedroom required?
- ob.connection.sendIQ(formsubmit,
- onSuccess,
- onError);
- } else {
- onNotSupported();
- }
- }, onError);
- },
- kick: function (jid) {
- var kickIQ = $iq({to: this.roomjid, type: 'set'})
- .c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
- .c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
- .c('reason').t('You have been kicked.').up().up().up();
-
- this.connection.sendIQ(
- kickIQ,
- function (result) {
- console.log('Kick participant with jid: ', jid, result);
- },
- function (error) {
- console.log('Kick participant error: ', error);
- });
- },
- sendPresence: function () {
- var pres = $pres({to: this.presMap['to'] });
- pres.c('x', {xmlns: this.presMap['xns']});
-
- if (this.presMap['password']) {
- pres.c('password').t(this.presMap['password']).up();
- }
-
- pres.up();
-
- // Send XEP-0115 'c' stanza that contains our capabilities info
- if (this.connection.caps) {
- this.connection.caps.node = config.clientNode;
- pres.c('c', this.connection.caps.generateCapsAttrs()).up();
- }
-
- pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
- .t(navigator.userAgent).up();
-
- if (this.presMap['bridgeIsDown']) {
- pres.c('bridgeIsDown').up();
- }
-
- if (this.presMap['email']) {
- pres.c('email').t(this.presMap['email']).up();
- }
-
- if (this.presMap['userId']) {
- pres.c('userId').t(this.presMap['userId']).up();
- }
-
- if (this.presMap['displayName']) {
- // XEP-0172
- pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
- .t(this.presMap['displayName']).up();
- }
-
- if (this.presMap['audions']) {
- pres.c('audiomuted', {xmlns: this.presMap['audions']})
- .t(this.presMap['audiomuted']).up();
- }
-
- if (this.presMap['videons']) {
- pres.c('videomuted', {xmlns: this.presMap['videons']})
- .t(this.presMap['videomuted']).up();
- }
-
- if (this.presMap['statsns']) {
- var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
- for (var stat in this.presMap["stats"])
- if (this.presMap["stats"][stat] != null)
- stats.c("stat", {name: stat, value: this.presMap["stats"][stat]}).up();
- pres.up();
- }
-
- if (this.presMap['prezins']) {
- pres.c('prezi',
- {xmlns: this.presMap['prezins'],
- 'url': this.presMap['preziurl']})
- .c('current').t(this.presMap['prezicurrent']).up().up();
- }
-
- if (this.presMap['etherpadns']) {
- pres.c('etherpad', {xmlns: this.presMap['etherpadns']})
- .t(this.presMap['etherpadname']).up();
- }
-
- if (this.presMap['medians']) {
- pres.c('media', {xmlns: this.presMap['medians']});
- var sourceNumber = 0;
- Object.keys(this.presMap).forEach(function (key) {
- if (key.indexOf('source') >= 0) {
- sourceNumber++;
- }
- });
- if (sourceNumber > 0)
- for (var i = 1; i <= sourceNumber / 3; i++) {
- pres.c('source',
- {type: this.presMap['source' + i + '_type'],
- ssrc: this.presMap['source' + i + '_ssrc'],
- direction: this.presMap['source' + i + '_direction']
- || 'sendrecv' }
- ).up();
- }
- }
-
- pres.up();
- this.connection.send(pres);
- },
- addDisplayNameToPresence: function (displayName) {
- this.presMap['displayName'] = displayName;
- },
- addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
- if (!this.presMap['medians'])
- this.presMap['medians'] = 'http://estos.de/ns/mjs';
-
- this.presMap['source' + sourceNumber + '_type'] = mtype;
- this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
- this.presMap['source' + sourceNumber + '_direction'] = direction;
- },
- clearPresenceMedia: function () {
- var self = this;
- Object.keys(this.presMap).forEach(function (key) {
- if (key.indexOf('source') != -1) {
- delete self.presMap[key];
- }
- });
- },
- addPreziToPresence: function (url, currentSlide) {
- this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
- this.presMap['preziurl'] = url;
- this.presMap['prezicurrent'] = currentSlide;
- },
- removePreziFromPresence: function () {
- delete this.presMap['prezins'];
- delete this.presMap['preziurl'];
- delete this.presMap['prezicurrent'];
- },
- addCurrentSlideToPresence: function (currentSlide) {
- this.presMap['prezicurrent'] = currentSlide;
- },
- getPrezi: function (roomjid) {
- return this.preziMap[roomjid];
- },
- addEtherpadToPresence: function (etherpadName) {
- this.presMap['etherpadns'] = 'http://jitsi.org/jitmeet/etherpad';
- this.presMap['etherpadname'] = etherpadName;
- },
- addAudioInfoToPresence: function (isMuted) {
- this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
- this.presMap['audiomuted'] = isMuted.toString();
- },
- addVideoInfoToPresence: function (isMuted) {
- this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
- this.presMap['videomuted'] = isMuted.toString();
- },
- addConnectionInfoToPresence: function (stats) {
- this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
- this.presMap['stats'] = stats;
- },
- findJidFromResource: function (resourceJid) {
- if (resourceJid &&
- resourceJid === Strophe.getResourceFromJid(this.myroomjid)) {
- return this.myroomjid;
- }
- var peerJid = null;
- Object.keys(this.members).some(function (jid) {
- peerJid = jid;
- return Strophe.getResourceFromJid(jid) === resourceJid;
- });
- return peerJid;
- },
- addBridgeIsDownToPresence: function () {
- this.presMap['bridgeIsDown'] = true;
- },
- addEmailToPresence: function (email) {
- this.presMap['email'] = email;
- },
- addUserIdToPresence: function (userId) {
- this.presMap['userId'] = userId;
- },
- isModerator: function () {
- return this.role === 'moderator';
- },
- getMemberRole: function (peerJid) {
- if (this.members[peerJid]) {
- return this.members[peerJid].role;
- }
- return null;
- },
- onParticipantLeft: function (jid) {
-
- eventEmitter.emit(XMPPEvents.MUC_LEFT, jid);
-
- this.connection.jingle.terminateByJid(jid);
-
- if (this.getPrezi(jid)) {
- $(document).trigger('presentationremoved.muc',
- [jid, this.getPrezi(jid)]);
- }
-
- Moderator.onMucLeft(jid);
- },
- parsePresence: function (from, memeber, pres) {
- if($(pres).find(">bridgeIsDown").length > 0 && !bridgeIsDown) {
- bridgeIsDown = true;
- eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
- }
-
- if(memeber.isFocus)
- return;
-
- var self = this;
- // Remove old ssrcs coming from the jid
- Object.keys(this.ssrc2jid).forEach(function (ssrc) {
- if (self.ssrc2jid[ssrc] == from) {
- delete self.ssrc2jid[ssrc];
- }
- });
-
- var changedStreams = [];
- $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
- //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
- var ssrcV = ssrc.getAttribute('ssrc');
- self.ssrc2jid[ssrcV] = from;
- JingleSession.notReceivedSSRCs.push(ssrcV);
-
-
- var type = ssrc.getAttribute('type');
-
- var direction = ssrc.getAttribute('direction');
-
- changedStreams.push({type: type, direction: direction});
-
- });
-
- eventEmitter.emit(XMPPEvents.CHANGED_STREAMS, from, changedStreams);
-
- var displayName = !config.displayJids
- ? memeber.displayName : Strophe.getResourceFromJid(from);
-
- if (displayName && displayName.length > 0)
- {
- eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
- }
-
-
- var id = $(pres).find('>userID').text();
- var email = $(pres).find('>email');
- if(email.length > 0) {
- id = email.text();
- }
-
- eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, id);
- }
- });
+},{"./moderator":54}],56:[function(require,module,exports){
+/* jshint -W117 */
+/* a simple MUC connection plugin
+ * can only handle a single MUC room
+ */
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var Moderator = require("./moderator");
+var JingleSession = require("./JingleSession");
+
+var bridgeIsDown = false;
+
+module.exports = function(XMPP, eventEmitter) {
+ Strophe.addConnectionPlugin('emuc', {
+ connection: null,
+ roomjid: null,
+ myroomjid: null,
+ members: {},
+ list_members: [], // so we can elect a new focus
+ presMap: {},
+ preziMap: {},
+ joined: false,
+ isOwner: false,
+ role: null,
+ focusMucJid: null,
+ ssrc2jid: {},
+ init: function (conn) {
+ this.connection = conn;
+ },
+ initPresenceMap: function (myroomjid) {
+ this.presMap['to'] = myroomjid;
+ this.presMap['xns'] = 'http://jabber.org/protocol/muc';
+ },
+ doJoin: function (jid, password) {
+ this.myroomjid = jid;
+
+ console.info("Joined MUC as " + this.myroomjid);
+
+ this.initPresenceMap(this.myroomjid);
+
+ if (!this.roomjid) {
+ this.roomjid = Strophe.getBareJidFromJid(jid);
+ // add handlers (just once)
+ this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
+ this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
+ this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
+ this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
+ }
+ if (password !== undefined) {
+ this.presMap['password'] = password;
+ }
+ this.sendPresence();
+ },
+ doLeave: function () {
+ console.log("do leave", this.myroomjid);
+ var pres = $pres({to: this.myroomjid, type: 'unavailable' });
+ this.presMap.length = 0;
+ this.connection.send(pres);
+ },
+ createNonAnonymousRoom: function () {
+ // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
+
+ var getForm = $iq({type: 'get', to: this.roomjid})
+ .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
+ .c('x', {xmlns: 'jabber:x:data', type: 'submit'});
+
+ var self = this;
+
+ this.connection.sendIQ(getForm, function (form) {
+
+ if (!$(form).find(
+ '>query>x[xmlns="jabber:x:data"]' +
+ '>field[var="muc#roomconfig_whois"]').length) {
+
+ console.error('non-anonymous rooms not supported');
+ return;
+ }
+
+ var formSubmit = $iq({to: this.roomjid, type: 'set'})
+ .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
+
+ formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
+
+ formSubmit.c('field', {'var': 'FORM_TYPE'})
+ .c('value')
+ .t('http://jabber.org/protocol/muc#roomconfig').up().up();
+
+ formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
+ .c('value').t('anyone').up().up();
+
+ self.connection.sendIQ(formSubmit);
+
+ }, function (error) {
+ console.error("Error getting room configuration form");
+ });
+ },
+ onPresence: function (pres) {
+ var from = pres.getAttribute('from');
+
+ // What is this for? A workaround for something?
+ if (pres.getAttribute('type')) {
+ return true;
+ }
+
+ // Parse etherpad tag.
+ var etherpad = $(pres).find('>etherpad');
+ if (etherpad.length) {
+ if (config.etherpad_base && !Moderator.isModerator()) {
+ eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
+ }
+ }
+
+ // Parse prezi tag.
+ var presentation = $(pres).find('>prezi');
+ if (presentation.length) {
+ var url = presentation.attr('url');
+ var current = presentation.find('>current').text();
+
+ console.log('presentation info received from', from, url);
+
+ if (this.preziMap[from] == null) {
+ this.preziMap[from] = url;
+
+ $(document).trigger('presentationadded.muc', [from, url, current]);
+ }
+ else {
+ $(document).trigger('gotoslide.muc', [from, url, current]);
+ }
+ }
+ else if (this.preziMap[from] != null) {
+ var url = this.preziMap[from];
+ delete this.preziMap[from];
+ $(document).trigger('presentationremoved.muc', [from, url]);
+ }
+
+ // Parse audio info tag.
+ var audioMuted = $(pres).find('>audiomuted');
+ if (audioMuted.length) {
+ $(document).trigger('audiomuted.muc', [from, audioMuted.text()]);
+ }
+
+ // Parse video info tag.
+ var videoMuted = $(pres).find('>videomuted');
+ if (videoMuted.length) {
+ $(document).trigger('videomuted.muc', [from, videoMuted.text()]);
+ }
+
+ var stats = $(pres).find('>stats');
+ if (stats.length) {
+ var statsObj = {};
+ Strophe.forEachChild(stats[0], "stat", function (el) {
+ statsObj[el.getAttribute("name")] = el.getAttribute("value");
+ });
+ eventEmitter.emit(XMPPEvents.REMOTE_STATS, from, statsObj);
+ }
+
+ // Parse status.
+ if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
+ this.isOwner = true;
+ this.createNonAnonymousRoom();
+ }
+
+ // Parse roles.
+ var member = {};
+ member.show = $(pres).find('>show').text();
+ member.status = $(pres).find('>status').text();
+ var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
+ member.affiliation = tmp.attr('affiliation');
+ member.role = tmp.attr('role');
+
+ // Focus recognition
+ member.jid = tmp.attr('jid');
+ member.isFocus = false;
+ if (member.jid
+ && member.jid.indexOf(Moderator.getFocusUserJid() + "/") == 0) {
+ member.isFocus = true;
+ }
+
+ var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
+ member.displayName = (nicktag.length > 0 ? nicktag.html() : null);
+
+ if (from == this.myroomjid) {
+ if (member.affiliation == 'owner') this.isOwner = true;
+ if (this.role !== member.role) {
+ this.role = member.role;
+
+ eventEmitter.emit(XMPPEvents.LOCALROLE_CHANGED,
+ from, member, pres, Moderator.isModerator());
+ }
+ if (!this.joined) {
+ this.joined = true;
+ eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
+ this.list_members.push(from);
+ }
+ } else if (this.members[from] === undefined) {
+ // new participant
+ this.members[from] = member;
+ this.list_members.push(from);
+ console.log('entered', from, member);
+ if (member.isFocus) {
+ this.focusMucJid = from;
+ console.info("Ignore focus: " + from + ", real JID: " + member.jid);
+ }
+ else {
+ var id = $(pres).find('>userID').text();
+ var email = $(pres).find('>email');
+ if (email.length > 0) {
+ id = email.text();
+ }
+ eventEmitter.emit(XMPPEvents.MUC_ENTER, from, id, member.displayName);
+ }
+ } else {
+ // Presence update for existing participant
+ // Watch role change:
+ if (this.members[from].role != member.role) {
+ this.members[from].role = member.role;
+ eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
+ member.role, member.displayName);
+ }
+ }
+
+ // Always trigger presence to update bindings
+ this.parsePresence(from, member, pres);
+
+ // Trigger status message update
+ if (member.status) {
+ eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
+ }
+
+ return true;
+ },
+ onPresenceUnavailable: function (pres) {
+ var from = pres.getAttribute('from');
+ // room destroyed ?
+ if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
+ '>destroy').length) {
+ var reason;
+ var reasonSelect = $(pres).find(
+ '>x[xmlns="http://jabber.org/protocol/muc#user"]' +
+ '>destroy>reason');
+ if (reasonSelect.length) {
+ reason = reasonSelect.text();
+ }
+ XMPP.disposeConference(false);
+ eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
+ return true;
+ }
+ // Status code 110 indicates that this notification is "self-presence".
+ if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
+ delete this.members[from];
+ this.list_members.splice(this.list_members.indexOf(from), 1);
+ this.onParticipantLeft(from);
+ }
+ // If the status code is 110 this means we're leaving and we would like
+ // to remove everyone else from our view, so we trigger the event.
+ else if (this.list_members.length > 1) {
+ for (var i = 0; i < this.list_members.length; i++) {
+ var member = this.list_members[i];
+ delete this.members[i];
+ this.list_members.splice(i, 1);
+ this.onParticipantLeft(member);
+ }
+ }
+ if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
+ $(document).trigger('kicked.muc', [from]);
+ if (this.myroomjid === from) {
+ XMPP.disposeConference(false);
+ eventEmitter.emit(XMPPEvents.KICKED);
+ }
+ }
+ return true;
+ },
+ onPresenceError: function (pres) {
+ var from = pres.getAttribute('from');
+ if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
+ console.log('on password required', from);
+ var self = this;
+ eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED, function (value) {
+ self.doJoin(from, value);
+ });
+ } else if ($(pres).find(
+ '>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
+ var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
+ if (toDomain === config.hosts.anonymousdomain) {
+ // enter the room by replying with 'not-authorized'. This would
+ // result in reconnection from authorized domain.
+ // We're either missing Jicofo/Prosody config for anonymous
+ // domains or something is wrong.
+// XMPP.promptLogin();
+ APP.UI.messageHandler.openReportDialog(null,
+ "dialog.joinError", pres);
+ } else {
+ console.warn('onPresError ', pres);
+ APP.UI.messageHandler.openReportDialog(null,
+ "dialog.connectError",
+ pres);
+ }
+ } else {
+ console.warn('onPresError ', pres);
+ APP.UI.messageHandler.openReportDialog(null,
+ "dialog.connectError",
+ pres);
+ }
+ return true;
+ },
+ sendMessage: function (body, nickname) {
+ var msg = $msg({to: this.roomjid, type: 'groupchat'});
+ msg.c('body', body).up();
+ if (nickname) {
+ msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
+ }
+ this.connection.send(msg);
+ eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
+ },
+ setSubject: function (subject) {
+ var msg = $msg({to: this.roomjid, type: 'groupchat'});
+ msg.c('subject', subject);
+ this.connection.send(msg);
+ console.log("topic changed to " + subject);
+ },
+ onMessage: function (msg) {
+ // FIXME: this is a hack. but jingle on muc makes nickchanges hard
+ var from = msg.getAttribute('from');
+ var nick =
+ $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
+ .text() ||
+ Strophe.getResourceFromJid(from);
+
+ var txt = $(msg).find('>body').text();
+ var type = msg.getAttribute("type");
+ if (type == "error") {
+ eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
+ $(msg).find('>text').text(), txt);
+ return true;
+ }
+
+ var subject = $(msg).find('>subject');
+ if (subject.length) {
+ var subjectText = subject.text();
+ if (subjectText || subjectText == "") {
+ eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
+ console.log("Subject is changed to " + subjectText);
+ }
+ }
+
+
+ if (txt) {
+ console.log('chat', nick, txt);
+ eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
+ from, nick, txt, this.myroomjid);
+ }
+ return true;
+ },
+ lockRoom: function (key, onSuccess, onError, onNotSupported) {
+ //http://xmpp.org/extensions/xep-0045.html#roomconfig
+ var ob = this;
+ this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
+ function (res) {
+ if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
+ var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
+ formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
+ formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
+ formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
+ // Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
+ formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
+ // FIXME: is muc#roomconfig_passwordprotectedroom required?
+ ob.connection.sendIQ(formsubmit,
+ onSuccess,
+ onError);
+ } else {
+ onNotSupported();
+ }
+ }, onError);
+ },
+ kick: function (jid) {
+ var kickIQ = $iq({to: this.roomjid, type: 'set'})
+ .c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
+ .c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
+ .c('reason').t('You have been kicked.').up().up().up();
+
+ this.connection.sendIQ(
+ kickIQ,
+ function (result) {
+ console.log('Kick participant with jid: ', jid, result);
+ },
+ function (error) {
+ console.log('Kick participant error: ', error);
+ });
+ },
+ sendPresence: function () {
+ var pres = $pres({to: this.presMap['to'] });
+ pres.c('x', {xmlns: this.presMap['xns']});
+
+ if (this.presMap['password']) {
+ pres.c('password').t(this.presMap['password']).up();
+ }
+
+ pres.up();
+
+ // Send XEP-0115 'c' stanza that contains our capabilities info
+ if (this.connection.caps) {
+ this.connection.caps.node = config.clientNode;
+ pres.c('c', this.connection.caps.generateCapsAttrs()).up();
+ }
+
+ pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
+ .t(navigator.userAgent).up();
+
+ if (this.presMap['bridgeIsDown']) {
+ pres.c('bridgeIsDown').up();
+ }
+
+ if (this.presMap['email']) {
+ pres.c('email').t(this.presMap['email']).up();
+ }
+
+ if (this.presMap['userId']) {
+ pres.c('userId').t(this.presMap['userId']).up();
+ }
+
+ if (this.presMap['displayName']) {
+ // XEP-0172
+ pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
+ .t(this.presMap['displayName']).up();
+ }
+
+ if (this.presMap['audions']) {
+ pres.c('audiomuted', {xmlns: this.presMap['audions']})
+ .t(this.presMap['audiomuted']).up();
+ }
+
+ if (this.presMap['videons']) {
+ pres.c('videomuted', {xmlns: this.presMap['videons']})
+ .t(this.presMap['videomuted']).up();
+ }
+
+ if (this.presMap['statsns']) {
+ var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
+ for (var stat in this.presMap["stats"])
+ if (this.presMap["stats"][stat] != null)
+ stats.c("stat", {name: stat, value: this.presMap["stats"][stat]}).up();
+ pres.up();
+ }
+
+ if (this.presMap['prezins']) {
+ pres.c('prezi',
+ {xmlns: this.presMap['prezins'],
+ 'url': this.presMap['preziurl']})
+ .c('current').t(this.presMap['prezicurrent']).up().up();
+ }
+
+ if (this.presMap['etherpadns']) {
+ pres.c('etherpad', {xmlns: this.presMap['etherpadns']})
+ .t(this.presMap['etherpadname']).up();
+ }
+
+ if (this.presMap['medians']) {
+ pres.c('media', {xmlns: this.presMap['medians']});
+ var sourceNumber = 0;
+ Object.keys(this.presMap).forEach(function (key) {
+ if (key.indexOf('source') >= 0) {
+ sourceNumber++;
+ }
+ });
+ if (sourceNumber > 0)
+ for (var i = 1; i <= sourceNumber / 3; i++) {
+ pres.c('source',
+ {type: this.presMap['source' + i + '_type'],
+ ssrc: this.presMap['source' + i + '_ssrc'],
+ direction: this.presMap['source' + i + '_direction']
+ || 'sendrecv' }
+ ).up();
+ }
+ }
+
+ pres.up();
+ this.connection.send(pres);
+ },
+ addDisplayNameToPresence: function (displayName) {
+ this.presMap['displayName'] = displayName;
+ },
+ addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
+ if (!this.presMap['medians'])
+ this.presMap['medians'] = 'http://estos.de/ns/mjs';
+
+ this.presMap['source' + sourceNumber + '_type'] = mtype;
+ this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
+ this.presMap['source' + sourceNumber + '_direction'] = direction;
+ },
+ clearPresenceMedia: function () {
+ var self = this;
+ Object.keys(this.presMap).forEach(function (key) {
+ if (key.indexOf('source') != -1) {
+ delete self.presMap[key];
+ }
+ });
+ },
+ addPreziToPresence: function (url, currentSlide) {
+ this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
+ this.presMap['preziurl'] = url;
+ this.presMap['prezicurrent'] = currentSlide;
+ },
+ removePreziFromPresence: function () {
+ delete this.presMap['prezins'];
+ delete this.presMap['preziurl'];
+ delete this.presMap['prezicurrent'];
+ },
+ addCurrentSlideToPresence: function (currentSlide) {
+ this.presMap['prezicurrent'] = currentSlide;
+ },
+ getPrezi: function (roomjid) {
+ return this.preziMap[roomjid];
+ },
+ addEtherpadToPresence: function (etherpadName) {
+ this.presMap['etherpadns'] = 'http://jitsi.org/jitmeet/etherpad';
+ this.presMap['etherpadname'] = etherpadName;
+ },
+ addAudioInfoToPresence: function (isMuted) {
+ this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
+ this.presMap['audiomuted'] = isMuted.toString();
+ },
+ addVideoInfoToPresence: function (isMuted) {
+ this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
+ this.presMap['videomuted'] = isMuted.toString();
+ },
+ addConnectionInfoToPresence: function (stats) {
+ this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
+ this.presMap['stats'] = stats;
+ },
+ findJidFromResource: function (resourceJid) {
+ if (resourceJid &&
+ resourceJid === Strophe.getResourceFromJid(this.myroomjid)) {
+ return this.myroomjid;
+ }
+ var peerJid = null;
+ Object.keys(this.members).some(function (jid) {
+ peerJid = jid;
+ return Strophe.getResourceFromJid(jid) === resourceJid;
+ });
+ return peerJid;
+ },
+ addBridgeIsDownToPresence: function () {
+ this.presMap['bridgeIsDown'] = true;
+ },
+ addEmailToPresence: function (email) {
+ this.presMap['email'] = email;
+ },
+ addUserIdToPresence: function (userId) {
+ this.presMap['userId'] = userId;
+ },
+ isModerator: function () {
+ return this.role === 'moderator';
+ },
+ getMemberRole: function (peerJid) {
+ if (this.members[peerJid]) {
+ return this.members[peerJid].role;
+ }
+ return null;
+ },
+ onParticipantLeft: function (jid) {
+
+ eventEmitter.emit(XMPPEvents.MUC_LEFT, jid);
+
+ this.connection.jingle.terminateByJid(jid);
+
+ if (this.getPrezi(jid)) {
+ $(document).trigger('presentationremoved.muc',
+ [jid, this.getPrezi(jid)]);
+ }
+
+ Moderator.onMucLeft(jid);
+ },
+ parsePresence: function (from, memeber, pres) {
+ if($(pres).find(">bridgeIsDown").length > 0 && !bridgeIsDown) {
+ bridgeIsDown = true;
+ eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
+ }
+
+ if(memeber.isFocus)
+ return;
+
+ var self = this;
+ // Remove old ssrcs coming from the jid
+ Object.keys(this.ssrc2jid).forEach(function (ssrc) {
+ if (self.ssrc2jid[ssrc] == from) {
+ delete self.ssrc2jid[ssrc];
+ }
+ });
+
+ var changedStreams = [];
+ $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
+ //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
+ var ssrcV = ssrc.getAttribute('ssrc');
+ self.ssrc2jid[ssrcV] = from;
+ JingleSession.notReceivedSSRCs.push(ssrcV);
+
+
+ var type = ssrc.getAttribute('type');
+
+ var direction = ssrc.getAttribute('direction');
+
+ changedStreams.push({type: type, direction: direction});
+
+ });
+
+ eventEmitter.emit(XMPPEvents.CHANGED_STREAMS, from, changedStreams);
+
+ var displayName = !config.displayJids
+ ? memeber.displayName : Strophe.getResourceFromJid(from);
+
+ if (displayName && displayName.length > 0)
+ {
+ eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
+ }
+
+
+ var id = $(pres).find('>userID').text();
+ var email = $(pres).find('>email');
+ if(email.length > 0) {
+ id = email.text();
+ }
+
+ eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, id);
+ }
+ });
+};
+
+
+},{"../../service/xmpp/XMPPEvents":98,"./JingleSession":49,"./moderator":54}],57:[function(require,module,exports){
+/* jshint -W117 */
+
+var JingleSession = require("./JingleSession");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+
+
+module.exports = function(XMPP, eventEmitter)
+{
+ function CallIncomingJingle(sid, connection) {
+ var sess = connection.jingle.sessions[sid];
+
+ // TODO: do we check activecall == null?
+ connection.jingle.activecall = sess;
+
+ eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
+
+ // TODO: check affiliation and/or role
+ console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
+ sess.usedrip = true; // not-so-naive trickle ice
+ sess.sendAnswer();
+ sess.accept();
+
+ };
+
+ Strophe.addConnectionPlugin('jingle', {
+ connection: null,
+ sessions: {},
+ jid2session: {},
+ ice_config: {iceServers: []},
+ pc_constraints: {},
+ activecall: null,
+ media_constraints: {
+ mandatory: {
+ 'OfferToReceiveAudio': true,
+ 'OfferToReceiveVideo': true
+ }
+ // MozDontOfferDataChannel: true when this is firefox
+ },
+ init: function (conn) {
+ this.connection = conn;
+ if (this.connection.disco) {
+ // http://xmpp.org/extensions/xep-0167.html#support
+ // http://xmpp.org/extensions/xep-0176.html#support
+ this.connection.disco.addFeature('urn:xmpp:jingle:1');
+ this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
+ this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
+ this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
+ this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
+ this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
+
+
+ // this is dealt with by SDP O/A so we don't need to annouce this
+ //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
+ //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
+ if (config.useRtcpMux) {
+ this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
+ }
+ if (config.useBundle) {
+ this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
+ }
+ //this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
+ }
+ this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
+ },
+ onJingle: function (iq) {
+ var sid = $(iq).find('jingle').attr('sid');
+ var action = $(iq).find('jingle').attr('action');
+ var fromJid = iq.getAttribute('from');
+ // send ack first
+ var ack = $iq({type: 'result',
+ to: fromJid,
+ id: iq.getAttribute('id')
+ });
+ console.log('on jingle ' + action + ' from ' + fromJid, iq);
+ var sess = this.sessions[sid];
+ if ('session-initiate' != action) {
+ if (sess === null) {
+ ack.type = 'error';
+ ack.c('error', {type: 'cancel'})
+ .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
+ .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
+ this.connection.send(ack);
+ return true;
+ }
+ // compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
+ // local jid is not checked
+ if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
+ console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
+ ack.type = 'error';
+ ack.c('error', {type: 'cancel'})
+ .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
+ .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
+ this.connection.send(ack);
+ return true;
+ }
+ } else if (sess !== undefined) {
+ // existing session with same session id
+ // this might be out-of-order if the sess.peerjid is the same as from
+ ack.type = 'error';
+ ack.c('error', {type: 'cancel'})
+ .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
+ console.warn('duplicate session id', sid);
+ this.connection.send(ack);
+ return true;
+ }
+ // FIXME: check for a defined action
+ this.connection.send(ack);
+ // see http://xmpp.org/extensions/xep-0166.html#concepts-session
+ switch (action) {
+ case 'session-initiate':
+ sess = new JingleSession(
+ $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
+ this.connection, XMPP);
+ // configure session
+
+ sess.media_constraints = this.media_constraints;
+ sess.pc_constraints = this.pc_constraints;
+ sess.ice_config = this.ice_config;
+
+ sess.initiate(fromJid, false);
+ // FIXME: setRemoteDescription should only be done when this call is to be accepted
+ sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
+
+ this.sessions[sess.sid] = sess;
+ this.jid2session[sess.peerjid] = sess;
+
+ // the callback should either
+ // .sendAnswer and .accept
+ // or .sendTerminate -- not necessarily synchronus
+ CallIncomingJingle(sess.sid, this.connection);
+ break;
+ case 'session-accept':
+ sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
+ sess.accept();
+ $(document).trigger('callaccepted.jingle', [sess.sid]);
+ break;
+ case 'session-terminate':
+ // If this is not the focus sending the terminate, we have
+ // nothing more to do here.
+ if (Object.keys(this.sessions).length < 1
+ || !(this.sessions[Object.keys(this.sessions)[0]]
+ instanceof JingleSession))
+ {
+ break;
+ }
+ console.log('terminating...', sess.sid);
+ sess.terminate();
+ this.terminate(sess.sid);
+ if ($(iq).find('>jingle>reason').length) {
+ $(document).trigger('callterminated.jingle', [
+ sess.sid,
+ sess.peerjid,
+ $(iq).find('>jingle>reason>:first')[0].tagName,
+ $(iq).find('>jingle>reason>text').text()
+ ]);
+ } else {
+ $(document).trigger('callterminated.jingle',
+ [sess.sid, sess.peerjid]);
+ }
+ break;
+ case 'transport-info':
+ sess.addIceCandidate($(iq).find('>jingle>content'));
+ break;
+ case 'session-info':
+ var affected;
+ if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
+ $(document).trigger('ringing.jingle', [sess.sid]);
+ } else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
+ affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
+ $(document).trigger('mute.jingle', [sess.sid, affected]);
+ } else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
+ affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
+ $(document).trigger('unmute.jingle', [sess.sid, affected]);
+ }
+ break;
+ case 'addsource': // FIXME: proprietary, un-jingleish
+ case 'source-add': // FIXME: proprietary
+ sess.addSource($(iq).find('>jingle>content'), fromJid);
+ break;
+ case 'removesource': // FIXME: proprietary, un-jingleish
+ case 'source-remove': // FIXME: proprietary
+ sess.removeSource($(iq).find('>jingle>content'), fromJid);
+ break;
+ default:
+ console.warn('jingle action not implemented', action);
+ break;
+ }
+ return true;
+ },
+ initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
+ var sess = new JingleSession(myjid || this.connection.jid,
+ Math.random().toString(36).substr(2, 12), // random string
+ this.connection, XMPP);
+ // configure session
+
+ sess.media_constraints = this.media_constraints;
+ sess.pc_constraints = this.pc_constraints;
+ sess.ice_config = this.ice_config;
+
+ sess.initiate(peerjid, true);
+ this.sessions[sess.sid] = sess;
+ this.jid2session[sess.peerjid] = sess;
+ sess.sendOffer();
+ return sess;
+ },
+ terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
+ if (sid === null || sid === undefined) {
+ for (sid in this.sessions) {
+ if (this.sessions[sid].state != 'ended') {
+ this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
+ this.sessions[sid].terminate();
+ }
+ delete this.jid2session[this.sessions[sid].peerjid];
+ delete this.sessions[sid];
+ }
+ } else if (this.sessions.hasOwnProperty(sid)) {
+ if (this.sessions[sid].state != 'ended') {
+ this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
+ this.sessions[sid].terminate();
+ }
+ delete this.jid2session[this.sessions[sid].peerjid];
+ delete this.sessions[sid];
+ }
+ },
+ // Used to terminate a session when an unavailable presence is received.
+ terminateByJid: function (jid) {
+ if (this.jid2session.hasOwnProperty(jid)) {
+ var sess = this.jid2session[jid];
+ if (sess) {
+ sess.terminate();
+ console.log('peer went away silently', jid);
+ delete this.sessions[sess.sid];
+ delete this.jid2session[jid];
+ $(document).trigger('callterminated.jingle',
+ [sess.sid, jid], 'gone');
+ }
+ }
+ },
+ terminateRemoteByJid: function (jid, reason) {
+ if (this.jid2session.hasOwnProperty(jid)) {
+ var sess = this.jid2session[jid];
+ if (sess) {
+ sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
+ sess.terminate();
+ console.log('terminate peer with jid', sess.sid, jid);
+ delete this.sessions[sess.sid];
+ delete this.jid2session[jid];
+ $(document).trigger('callterminated.jingle',
+ [sess.sid, jid, 'kicked']);
+ }
+ }
+ },
+ getStunAndTurnCredentials: function () {
+ // get stun and turn configuration from server via xep-0215
+ // uses time-limited credentials as described in
+ // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
+ //
+ // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
+ // for a prosody module which implements this
+ //
+ // currently, this doesn't work with updateIce and therefore credentials with a long
+ // validity have to be fetched before creating the peerconnection
+ // TODO: implement refresh via updateIce as described in
+ // https://code.google.com/p/webrtc/issues/detail?id=1650
+ var self = this;
+ this.connection.sendIQ(
+ $iq({type: 'get', to: this.connection.domain})
+ .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
+ function (res) {
+ var iceservers = [];
+ $(res).find('>services>service').each(function (idx, el) {
+ el = $(el);
+ var dict = {};
+ var type = el.attr('type');
+ switch (type) {
+ case 'stun':
+ dict.url = 'stun:' + el.attr('host');
+ if (el.attr('port')) {
+ dict.url += ':' + el.attr('port');
+ }
+ iceservers.push(dict);
+ break;
+ case 'turn':
+ case 'turns':
+ dict.url = type + ':';
+ if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
+ if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
+ dict.url += el.attr('username') + '@';
+ } else {
+ dict.username = el.attr('username'); // only works in M28
+ }
+ }
+ dict.url += el.attr('host');
+ if (el.attr('port') && el.attr('port') != '3478') {
+ dict.url += ':' + el.attr('port');
+ }
+ if (el.attr('transport') && el.attr('transport') != 'udp') {
+ dict.url += '?transport=' + el.attr('transport');
+ }
+ if (el.attr('password')) {
+ dict.credential = el.attr('password');
+ }
+ iceservers.push(dict);
+ break;
+ }
+ });
+ self.ice_config.iceServers = iceservers;
+ },
+ function (err) {
+ console.warn('getting turn credentials failed', err);
+ console.warn('is mod_turncredentials or similar installed?');
+ }
+ );
+ // implement push?
+ },
+
+ /**
+ * Populates the log data
+ */
+ populateData: function () {
+ var data = {};
+ Object.keys(this.sessions).forEach(function (sid) {
+ var session = this.sessions[sid];
+ if (session.peerconnection && session.peerconnection.updateLog) {
+ // FIXME: should probably be a .dump call
+ data["jingle_" + session.sid] = {
+ updateLog: session.peerconnection.updateLog,
+ stats: session.peerconnection.stats,
+ url: window.location.href
+ };
+ }
+ });
+ return data;
+ }
+ });
+};
+
+
+},{"../../service/xmpp/XMPPEvents":98,"./JingleSession":49}],58:[function(require,module,exports){
+/* global Strophe */
+module.exports = function () {
+
+ Strophe.addConnectionPlugin('logger', {
+ // logs raw stanzas and makes them available for download as JSON
+ connection: null,
+ log: [],
+ init: function (conn) {
+ this.connection = conn;
+ this.connection.rawInput = this.log_incoming.bind(this);
+ this.connection.rawOutput = this.log_outgoing.bind(this);
+ },
+ log_incoming: function (stanza) {
+ this.log.push([new Date().getTime(), 'incoming', stanza]);
+ },
+ log_outgoing: function (stanza) {
+ this.log.push([new Date().getTime(), 'outgoing', stanza]);
+ }
+ });
};
-
-
-},{"../../service/xmpp/XMPPEvents":97,"./JingleSession":48,"./moderator":53}],56:[function(require,module,exports){
-/* jshint -W117 */
-
-var JingleSession = require("./JingleSession");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-
-
-module.exports = function(XMPP, eventEmitter)
-{
- function CallIncomingJingle(sid, connection) {
- var sess = connection.jingle.sessions[sid];
-
- // TODO: do we check activecall == null?
- connection.jingle.activecall = sess;
-
- eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
-
- // TODO: check affiliation and/or role
- console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
- sess.usedrip = true; // not-so-naive trickle ice
- sess.sendAnswer();
- sess.accept();
-
- };
-
- Strophe.addConnectionPlugin('jingle', {
- connection: null,
- sessions: {},
- jid2session: {},
- ice_config: {iceServers: []},
- pc_constraints: {},
- activecall: null,
- media_constraints: {
- mandatory: {
- 'OfferToReceiveAudio': true,
- 'OfferToReceiveVideo': true
- }
- // MozDontOfferDataChannel: true when this is firefox
- },
- init: function (conn) {
- this.connection = conn;
- if (this.connection.disco) {
- // http://xmpp.org/extensions/xep-0167.html#support
- // http://xmpp.org/extensions/xep-0176.html#support
- this.connection.disco.addFeature('urn:xmpp:jingle:1');
- this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
- this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
- this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
- this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
- this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
-
-
- // this is dealt with by SDP O/A so we don't need to annouce this
- //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
- //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
- if (config.useRtcpMux) {
- this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
- }
- if (config.useBundle) {
- this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
- }
- //this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
- }
- this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
- },
- onJingle: function (iq) {
- var sid = $(iq).find('jingle').attr('sid');
- var action = $(iq).find('jingle').attr('action');
- var fromJid = iq.getAttribute('from');
- // send ack first
- var ack = $iq({type: 'result',
- to: fromJid,
- id: iq.getAttribute('id')
- });
- console.log('on jingle ' + action + ' from ' + fromJid, iq);
- var sess = this.sessions[sid];
- if ('session-initiate' != action) {
- if (sess === null) {
- ack.type = 'error';
- ack.c('error', {type: 'cancel'})
- .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
- .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
- this.connection.send(ack);
- return true;
- }
- // compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
- // local jid is not checked
- if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
- console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
- ack.type = 'error';
- ack.c('error', {type: 'cancel'})
- .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
- .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
- this.connection.send(ack);
- return true;
- }
- } else if (sess !== undefined) {
- // existing session with same session id
- // this might be out-of-order if the sess.peerjid is the same as from
- ack.type = 'error';
- ack.c('error', {type: 'cancel'})
- .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
- console.warn('duplicate session id', sid);
- this.connection.send(ack);
- return true;
- }
- // FIXME: check for a defined action
- this.connection.send(ack);
- // see http://xmpp.org/extensions/xep-0166.html#concepts-session
- switch (action) {
- case 'session-initiate':
- sess = new JingleSession(
- $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
- this.connection, XMPP);
- // configure session
-
- sess.media_constraints = this.media_constraints;
- sess.pc_constraints = this.pc_constraints;
- sess.ice_config = this.ice_config;
-
- sess.initiate(fromJid, false);
- // FIXME: setRemoteDescription should only be done when this call is to be accepted
- sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
-
- this.sessions[sess.sid] = sess;
- this.jid2session[sess.peerjid] = sess;
-
- // the callback should either
- // .sendAnswer and .accept
- // or .sendTerminate -- not necessarily synchronus
- CallIncomingJingle(sess.sid, this.connection);
- break;
- case 'session-accept':
- sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
- sess.accept();
- $(document).trigger('callaccepted.jingle', [sess.sid]);
- break;
- case 'session-terminate':
- // If this is not the focus sending the terminate, we have
- // nothing more to do here.
- if (Object.keys(this.sessions).length < 1
- || !(this.sessions[Object.keys(this.sessions)[0]]
- instanceof JingleSession))
- {
- break;
- }
- console.log('terminating...', sess.sid);
- sess.terminate();
- this.terminate(sess.sid);
- if ($(iq).find('>jingle>reason').length) {
- $(document).trigger('callterminated.jingle', [
- sess.sid,
- sess.peerjid,
- $(iq).find('>jingle>reason>:first')[0].tagName,
- $(iq).find('>jingle>reason>text').text()
- ]);
- } else {
- $(document).trigger('callterminated.jingle',
- [sess.sid, sess.peerjid]);
- }
- break;
- case 'transport-info':
- sess.addIceCandidate($(iq).find('>jingle>content'));
- break;
- case 'session-info':
- var affected;
- if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
- $(document).trigger('ringing.jingle', [sess.sid]);
- } else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
- affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
- $(document).trigger('mute.jingle', [sess.sid, affected]);
- } else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
- affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
- $(document).trigger('unmute.jingle', [sess.sid, affected]);
- }
- break;
- case 'addsource': // FIXME: proprietary, un-jingleish
- case 'source-add': // FIXME: proprietary
- sess.addSource($(iq).find('>jingle>content'), fromJid);
- break;
- case 'removesource': // FIXME: proprietary, un-jingleish
- case 'source-remove': // FIXME: proprietary
- sess.removeSource($(iq).find('>jingle>content'), fromJid);
- break;
- default:
- console.warn('jingle action not implemented', action);
- break;
- }
- return true;
- },
- initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
- var sess = new JingleSession(myjid || this.connection.jid,
- Math.random().toString(36).substr(2, 12), // random string
- this.connection, XMPP);
- // configure session
-
- sess.media_constraints = this.media_constraints;
- sess.pc_constraints = this.pc_constraints;
- sess.ice_config = this.ice_config;
-
- sess.initiate(peerjid, true);
- this.sessions[sess.sid] = sess;
- this.jid2session[sess.peerjid] = sess;
- sess.sendOffer();
- return sess;
- },
- terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
- if (sid === null || sid === undefined) {
- for (sid in this.sessions) {
- if (this.sessions[sid].state != 'ended') {
- this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
- this.sessions[sid].terminate();
- }
- delete this.jid2session[this.sessions[sid].peerjid];
- delete this.sessions[sid];
- }
- } else if (this.sessions.hasOwnProperty(sid)) {
- if (this.sessions[sid].state != 'ended') {
- this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
- this.sessions[sid].terminate();
- }
- delete this.jid2session[this.sessions[sid].peerjid];
- delete this.sessions[sid];
- }
- },
- // Used to terminate a session when an unavailable presence is received.
- terminateByJid: function (jid) {
- if (this.jid2session.hasOwnProperty(jid)) {
- var sess = this.jid2session[jid];
- if (sess) {
- sess.terminate();
- console.log('peer went away silently', jid);
- delete this.sessions[sess.sid];
- delete this.jid2session[jid];
- $(document).trigger('callterminated.jingle',
- [sess.sid, jid], 'gone');
- }
- }
- },
- terminateRemoteByJid: function (jid, reason) {
- if (this.jid2session.hasOwnProperty(jid)) {
- var sess = this.jid2session[jid];
- if (sess) {
- sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
- sess.terminate();
- console.log('terminate peer with jid', sess.sid, jid);
- delete this.sessions[sess.sid];
- delete this.jid2session[jid];
- $(document).trigger('callterminated.jingle',
- [sess.sid, jid, 'kicked']);
- }
- }
- },
- getStunAndTurnCredentials: function () {
- // get stun and turn configuration from server via xep-0215
- // uses time-limited credentials as described in
- // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
- //
- // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
- // for a prosody module which implements this
- //
- // currently, this doesn't work with updateIce and therefore credentials with a long
- // validity have to be fetched before creating the peerconnection
- // TODO: implement refresh via updateIce as described in
- // https://code.google.com/p/webrtc/issues/detail?id=1650
- var self = this;
- this.connection.sendIQ(
- $iq({type: 'get', to: this.connection.domain})
- .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
- function (res) {
- var iceservers = [];
- $(res).find('>services>service').each(function (idx, el) {
- el = $(el);
- var dict = {};
- var type = el.attr('type');
- switch (type) {
- case 'stun':
- dict.url = 'stun:' + el.attr('host');
- if (el.attr('port')) {
- dict.url += ':' + el.attr('port');
- }
- iceservers.push(dict);
- break;
- case 'turn':
- case 'turns':
- dict.url = type + ':';
- if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
- if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
- dict.url += el.attr('username') + '@';
- } else {
- dict.username = el.attr('username'); // only works in M28
- }
- }
- dict.url += el.attr('host');
- if (el.attr('port') && el.attr('port') != '3478') {
- dict.url += ':' + el.attr('port');
- }
- if (el.attr('transport') && el.attr('transport') != 'udp') {
- dict.url += '?transport=' + el.attr('transport');
- }
- if (el.attr('password')) {
- dict.credential = el.attr('password');
- }
- iceservers.push(dict);
- break;
- }
- });
- self.ice_config.iceServers = iceservers;
- },
- function (err) {
- console.warn('getting turn credentials failed', err);
- console.warn('is mod_turncredentials or similar installed?');
- }
- );
- // implement push?
- },
-
- /**
- * Populates the log data
- */
- populateData: function () {
- var data = {};
- Object.keys(this.sessions).forEach(function (sid) {
- var session = this.sessions[sid];
- if (session.peerconnection && session.peerconnection.updateLog) {
- // FIXME: should probably be a .dump call
- data["jingle_" + session.sid] = {
- updateLog: session.peerconnection.updateLog,
- stats: session.peerconnection.stats,
- url: window.location.href
- };
- }
- });
- return data;
- }
- });
-};
-
-
-},{"../../service/xmpp/XMPPEvents":97,"./JingleSession":48}],57:[function(require,module,exports){
-/* global Strophe */
-module.exports = function () {
-
- Strophe.addConnectionPlugin('logger', {
- // logs raw stanzas and makes them available for download as JSON
- connection: null,
- log: [],
- init: function (conn) {
- this.connection = conn;
- this.connection.rawInput = this.log_incoming.bind(this);
- this.connection.rawOutput = this.log_outgoing.bind(this);
- },
- log_incoming: function (stanza) {
- this.log.push([new Date().getTime(), 'incoming', stanza]);
- },
- log_outgoing: function (stanza) {
- this.log.push([new Date().getTime(), 'outgoing', stanza]);
- }
- });
-};
-},{}],58:[function(require,module,exports){
-/* global $, $iq, config, connection, focusMucJid, forceMuted,
- setAudioMuted, Strophe */
-/**
- * Moderate connection plugin.
- */
-module.exports = function (XMPP) {
- Strophe.addConnectionPlugin('moderate', {
- connection: null,
- init: function (conn) {
- this.connection = conn;
-
- this.connection.addHandler(this.onMute.bind(this),
- 'http://jitsi.org/jitmeet/audio',
- 'iq',
- 'set',
- null,
- null);
- },
- setMute: function (jid, mute) {
- console.info("set mute", mute);
- var iqToFocus = $iq({to: this.connection.emuc.focusMucJid, type: 'set'})
- .c('mute', {
- xmlns: 'http://jitsi.org/jitmeet/audio',
- jid: jid
- })
- .t(mute.toString())
- .up();
-
- this.connection.sendIQ(
- iqToFocus,
- function (result) {
- console.log('set mute', result);
- },
- function (error) {
- console.log('set mute error', error);
- });
- },
- onMute: function (iq) {
- var from = iq.getAttribute('from');
- if (from !== this.connection.emuc.focusMucJid) {
- console.warn("Ignored mute from non focus peer");
- return false;
- }
- var mute = $(iq).find('mute');
- if (mute.length) {
- var doMuteAudio = mute.text() === "true";
- APP.UI.setAudioMuted(doMuteAudio);
- XMPP.forceMuted = doMuteAudio;
- }
- return true;
- },
- eject: function (jid) {
- // We're not the focus, so can't terminate
- //connection.jingle.terminateRemoteByJid(jid, 'kick');
- this.connection.emuc.kick(jid);
- }
- });
-}
},{}],59:[function(require,module,exports){
-/* jshint -W117 */
-module.exports = function() {
- Strophe.addConnectionPlugin('rayo',
- {
- RAYO_XMLNS: 'urn:xmpp:rayo:1',
- connection: null,
- init: function (conn) {
- this.connection = conn;
- if (this.connection.disco) {
- this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
- }
-
- this.connection.addHandler(
- this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set', null, null);
- },
- onRayo: function (iq) {
- console.info("Rayo IQ", iq);
- },
- dial: function (to, from, roomName, roomPass) {
- var self = this;
- var req = $iq(
- {
- type: 'set',
- to: this.connection.emuc.focusMucJid
- }
- );
- req.c('dial',
- {
- xmlns: this.RAYO_XMLNS,
- to: to,
- from: from
- });
- req.c('header',
- {
- name: 'JvbRoomName',
- value: roomName
- }).up();
-
- if (roomPass !== null && roomPass.length) {
-
- req.c('header',
- {
- name: 'JvbRoomPassword',
- value: roomPass
- }).up();
- }
-
- this.connection.sendIQ(
- req,
- function (result) {
- console.info('Dial result ', result);
-
- var resource = $(result).find('ref').attr('uri');
- this.call_resource = resource.substr('xmpp:'.length);
- console.info(
- "Received call resource: " + this.call_resource);
- },
- function (error) {
- console.info('Dial error ', error);
- }
- );
- },
- hang_up: function () {
- if (!this.call_resource) {
- console.warn("No call in progress");
- return;
- }
-
- var self = this;
- var req = $iq(
- {
- type: 'set',
- to: this.call_resource
- }
- );
- req.c('hangup',
- {
- xmlns: this.RAYO_XMLNS
- });
-
- this.connection.sendIQ(
- req,
- function (result) {
- console.info('Hangup result ', result);
- self.call_resource = null;
- },
- function (error) {
- console.info('Hangup error ', error);
- self.call_resource = null;
- }
- );
- }
- }
- );
-};
-
+/* global $, $iq, config, connection, focusMucJid, forceMuted,
+ setAudioMuted, Strophe */
+/**
+ * Moderate connection plugin.
+ */
+module.exports = function (XMPP) {
+ Strophe.addConnectionPlugin('moderate', {
+ connection: null,
+ init: function (conn) {
+ this.connection = conn;
+
+ this.connection.addHandler(this.onMute.bind(this),
+ 'http://jitsi.org/jitmeet/audio',
+ 'iq',
+ 'set',
+ null,
+ null);
+ },
+ setMute: function (jid, mute) {
+ console.info("set mute", mute);
+ var iqToFocus = $iq({to: this.connection.emuc.focusMucJid, type: 'set'})
+ .c('mute', {
+ xmlns: 'http://jitsi.org/jitmeet/audio',
+ jid: jid
+ })
+ .t(mute.toString())
+ .up();
+
+ this.connection.sendIQ(
+ iqToFocus,
+ function (result) {
+ console.log('set mute', result);
+ },
+ function (error) {
+ console.log('set mute error', error);
+ });
+ },
+ onMute: function (iq) {
+ var from = iq.getAttribute('from');
+ if (from !== this.connection.emuc.focusMucJid) {
+ console.warn("Ignored mute from non focus peer");
+ return false;
+ }
+ var mute = $(iq).find('mute');
+ if (mute.length) {
+ var doMuteAudio = mute.text() === "true";
+ APP.UI.setAudioMuted(doMuteAudio);
+ XMPP.forceMuted = doMuteAudio;
+ }
+ return true;
+ },
+ eject: function (jid) {
+ // We're not the focus, so can't terminate
+ //connection.jingle.terminateRemoteByJid(jid, 'kick');
+ this.connection.emuc.kick(jid);
+ }
+ });
+}
},{}],60:[function(require,module,exports){
-/**
- * Strophe logger implementation. Logs from level WARN and above.
- */
-module.exports = function () {
-
- Strophe.log = function (level, msg) {
- switch (level) {
- case Strophe.LogLevel.WARN:
- console.warn("Strophe: " + msg);
- break;
- case Strophe.LogLevel.ERROR:
- case Strophe.LogLevel.FATAL:
- console.error("Strophe: " + msg);
- break;
- }
- };
-
- Strophe.getStatusString = function (status) {
- switch (status) {
- case Strophe.Status.ERROR:
- return "ERROR";
- case Strophe.Status.CONNECTING:
- return "CONNECTING";
- case Strophe.Status.CONNFAIL:
- return "CONNFAIL";
- case Strophe.Status.AUTHENTICATING:
- return "AUTHENTICATING";
- case Strophe.Status.AUTHFAIL:
- return "AUTHFAIL";
- case Strophe.Status.CONNECTED:
- return "CONNECTED";
- case Strophe.Status.DISCONNECTED:
- return "DISCONNECTED";
- case Strophe.Status.DISCONNECTING:
- return "DISCONNECTING";
- case Strophe.Status.ATTACHED:
- return "ATTACHED";
- default:
- return "unknown";
- }
- };
-};
+/* jshint -W117 */
+module.exports = function() {
+ Strophe.addConnectionPlugin('rayo',
+ {
+ RAYO_XMLNS: 'urn:xmpp:rayo:1',
+ connection: null,
+ init: function (conn) {
+ this.connection = conn;
+ if (this.connection.disco) {
+ this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
+ }
+
+ this.connection.addHandler(
+ this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set', null, null);
+ },
+ onRayo: function (iq) {
+ console.info("Rayo IQ", iq);
+ },
+ dial: function (to, from, roomName, roomPass) {
+ var self = this;
+ var req = $iq(
+ {
+ type: 'set',
+ to: this.connection.emuc.focusMucJid
+ }
+ );
+ req.c('dial',
+ {
+ xmlns: this.RAYO_XMLNS,
+ to: to,
+ from: from
+ });
+ req.c('header',
+ {
+ name: 'JvbRoomName',
+ value: roomName
+ }).up();
+
+ if (roomPass !== null && roomPass.length) {
+
+ req.c('header',
+ {
+ name: 'JvbRoomPassword',
+ value: roomPass
+ }).up();
+ }
+
+ this.connection.sendIQ(
+ req,
+ function (result) {
+ console.info('Dial result ', result);
+
+ var resource = $(result).find('ref').attr('uri');
+ this.call_resource = resource.substr('xmpp:'.length);
+ console.info(
+ "Received call resource: " + this.call_resource);
+ },
+ function (error) {
+ console.info('Dial error ', error);
+ }
+ );
+ },
+ hang_up: function () {
+ if (!this.call_resource) {
+ console.warn("No call in progress");
+ return;
+ }
+
+ var self = this;
+ var req = $iq(
+ {
+ type: 'set',
+ to: this.call_resource
+ }
+ );
+ req.c('hangup',
+ {
+ xmlns: this.RAYO_XMLNS
+ });
+
+ this.connection.sendIQ(
+ req,
+ function (result) {
+ console.info('Hangup result ', result);
+ self.call_resource = null;
+ },
+ function (error) {
+ console.info('Hangup error ', error);
+ self.call_resource = null;
+ }
+ );
+ }
+ }
+ );
+};
},{}],61:[function(require,module,exports){
-/* global $, APP, config, Strophe*/
-var Moderator = require("./moderator");
-var EventEmitter = require("events");
-var Recording = require("./recording");
-var SDP = require("./SDP");
-var Pako = require("pako");
-var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
-var UIEvents = require("../../service/UI/UIEvents");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+/**
+ * Strophe logger implementation. Logs from level WARN and above.
+ */
+module.exports = function () {
+
+ Strophe.log = function (level, msg) {
+ switch (level) {
+ case Strophe.LogLevel.WARN:
+ console.warn("Strophe: " + msg);
+ break;
+ case Strophe.LogLevel.ERROR:
+ case Strophe.LogLevel.FATAL:
+ console.error("Strophe: " + msg);
+ break;
+ }
+ };
+
+ Strophe.getStatusString = function (status) {
+ switch (status) {
+ case Strophe.Status.ERROR:
+ return "ERROR";
+ case Strophe.Status.CONNECTING:
+ return "CONNECTING";
+ case Strophe.Status.CONNFAIL:
+ return "CONNFAIL";
+ case Strophe.Status.AUTHENTICATING:
+ return "AUTHENTICATING";
+ case Strophe.Status.AUTHFAIL:
+ return "AUTHFAIL";
+ case Strophe.Status.CONNECTED:
+ return "CONNECTED";
+ case Strophe.Status.DISCONNECTED:
+ return "DISCONNECTED";
+ case Strophe.Status.DISCONNECTING:
+ return "DISCONNECTING";
+ case Strophe.Status.ATTACHED:
+ return "ATTACHED";
+ default:
+ return "unknown";
+ }
+ };
+};
-var eventEmitter = new EventEmitter();
-var connection = null;
-var authenticatedUser = false;
+},{}],62:[function(require,module,exports){
+/* global $, APP, config, Strophe*/
+var Moderator = require("./moderator");
+var EventEmitter = require("events");
+var Recording = require("./recording");
+var SDP = require("./SDP");
+var Pako = require("pako");
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
+var UIEvents = require("../../service/UI/UIEvents");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+
+var eventEmitter = new EventEmitter();
+var connection = null;
+var authenticatedUser = false;
+
+function connect(jid, password) {
+ connection = XMPP.createConnection();
+ Moderator.setConnection(connection);
+
+ if (connection.disco) {
+ // for chrome, add multistream cap
+ }
+ connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
+ if (config.useIPv6) {
+ // https://code.google.com/p/webrtc/issues/detail?id=2828
+ if (!connection.jingle.pc_constraints.optional)
+ connection.jingle.pc_constraints.optional = [];
+ connection.jingle.pc_constraints.optional.push({googIPv6: true});
+ }
+
+ var anonymousConnectionFailed = false;
+ connection.connect(jid, password, function (status, msg) {
+ console.log('Strophe status changed to',
+ Strophe.getStatusString(status));
+ if (status === Strophe.Status.CONNECTED) {
+ if (config.useStunTurn) {
+ connection.jingle.getStunAndTurnCredentials();
+ }
+
+ console.info("My Jabber ID: " + connection.jid);
+
+ if(password)
+ authenticatedUser = true;
+ maybeDoJoin();
+ } else if (status === Strophe.Status.CONNFAIL) {
+ if(msg === 'x-strophe-bad-non-anon-jid') {
+ anonymousConnectionFailed = true;
+ }
+ } else if (status === Strophe.Status.DISCONNECTED) {
+ if(anonymousConnectionFailed) {
+ // prompt user for username and password
+ XMPP.promptLogin();
+ }
+ } else if (status === Strophe.Status.AUTHFAIL) {
+ // wrong password or username, prompt user
+ XMPP.promptLogin();
+
+ }
+ });
+}
+
+
+
+function maybeDoJoin() {
+ if (connection && connection.connected &&
+ Strophe.getResourceFromJid(connection.jid)
+ && (APP.RTC.localAudio || APP.RTC.localVideo)) {
+ // .connected is true while connecting?
+ doJoin();
+ }
+}
+
+function doJoin() {
+ var roomName = APP.UI.generateRoomName();
+
+ Moderator.allocateConferenceFocus(
+ roomName, APP.UI.checkForNicknameAndJoin);
+}
+
+function initStrophePlugins()
+{
+ require("./strophe.emuc")(XMPP, eventEmitter);
+ require("./strophe.jingle")(XMPP, eventEmitter);
+ require("./strophe.moderate")(XMPP);
+ require("./strophe.util")();
+ require("./strophe.rayo")();
+ require("./strophe.logger")();
+}
+
+function registerListeners() {
+ APP.RTC.addStreamListener(maybeDoJoin,
+ StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
+ APP.UI.addListener(UIEvents.NICKNAME_CHANGED, function (nickname) {
+ XMPP.addToPresence("displayName", nickname);
+ });
+}
+
+function setupEvents() {
+ $(window).bind('beforeunload', function () {
+ if (connection && connection.connected) {
+ // ensure signout
+ $.ajax({
+ type: 'POST',
+ url: config.bosh,
+ async: false,
+ cache: false,
+ contentType: 'application/xml',
+ data: "" +
+ " " +
+ "",
+ success: function (data) {
+ console.log('signed out');
+ console.log(data);
+ },
+ error: function (XMLHttpRequest, textStatus, errorThrown) {
+ console.log('signout error',
+ textStatus + ' (' + errorThrown + ')');
+ }
+ });
+ }
+ XMPP.disposeConference(true);
+ });
+}
+
+var XMPP = {
+ sessionTerminated: false,
+
+ /**
+ * XMPP connection status
+ */
+ Status: Strophe.Status,
+
+ /**
+ * Remembers if we were muted by the focus.
+ * @type {boolean}
+ */
+ forceMuted: false,
+ start: function () {
+ setupEvents();
+ initStrophePlugins();
+ registerListeners();
+ Moderator.init(this, eventEmitter);
+ var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
+ // Force authenticated domain if room is appended with '?login=true'
+ if (config.hosts.anonymousdomain &&
+ window.location.search.indexOf("login=true") !== -1) {
+ configDomain = config.hosts.domain;
+ }
+ var jid = configDomain || window.location.hostname;
+ connect(jid, null);
+ },
+ createConnection: function () {
+ var bosh = config.bosh || '/http-bind';
+
+ return new Strophe.Connection(bosh);
+ },
+ getStatusString: function (status) {
+ return Strophe.getStatusString(status);
+ },
+ promptLogin: function () {
+ // FIXME: re-use LoginDialog which supports retries
+ APP.UI.showLoginPopup(connect);
+ },
+ joinRoom: function(roomName, useNicks, nick)
+ {
+ var roomjid;
+ roomjid = roomName;
+
+ if (useNicks) {
+ if (nick) {
+ roomjid += '/' + nick;
+ } else {
+ roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
+ }
+ } else {
+
+ var tmpJid = Strophe.getNodeFromJid(connection.jid);
+
+ if(!authenticatedUser)
+ tmpJid = tmpJid.substr(0, 8);
+
+ roomjid += '/' + tmpJid;
+ }
+ connection.emuc.doJoin(roomjid);
+ },
+ myJid: function () {
+ if(!connection)
+ return null;
+ return connection.emuc.myroomjid;
+ },
+ myResource: function () {
+ if(!connection || ! connection.emuc.myroomjid)
+ return null;
+ return Strophe.getResourceFromJid(connection.emuc.myroomjid);
+ },
+ disposeConference: function (onUnload) {
+ eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
+ var handler = connection.jingle.activecall;
+ if (handler && handler.peerconnection) {
+ // FIXME: probably removing streams is not required and close() should
+ // be enough
+ if (APP.RTC.localAudio) {
+ handler.peerconnection.removeStream(APP.RTC.localAudio.getOriginalStream(), onUnload);
+ }
+ if (APP.RTC.localVideo) {
+ handler.peerconnection.removeStream(APP.RTC.localVideo.getOriginalStream(), onUnload);
+ }
+ handler.peerconnection.close();
+ }
+ connection.jingle.activecall = null;
+ if(!onUnload)
+ {
+ this.sessionTerminated = true;
+ connection.emuc.doLeave();
+ }
+ },
+ addListener: function(type, listener)
+ {
+ eventEmitter.on(type, listener);
+ },
+ removeListener: function (type, listener) {
+ eventEmitter.removeListener(type, listener);
+ },
+ allocateConferenceFocus: function(roomName, callback) {
+ Moderator.allocateConferenceFocus(roomName, callback);
+ },
+ getLoginUrl: function (roomName, callback) {
+ Moderator.getLoginUrl(roomName, callback);
+ },
+ getPopupLoginUrl: function (roomName, callback) {
+ Moderator.getPopupLoginUrl(roomName, callback);
+ },
+ isModerator: function () {
+ return Moderator.isModerator();
+ },
+ isSipGatewayEnabled: function () {
+ return Moderator.isSipGatewayEnabled();
+ },
+ isExternalAuthEnabled: function () {
+ return Moderator.isExternalAuthEnabled();
+ },
+ switchStreams: function (stream, oldStream, callback) {
+ if (connection && connection.jingle.activecall) {
+ // FIXME: will block switchInProgress on true value in case of exception
+ connection.jingle.activecall.switchStreams(stream, oldStream, callback);
+ } else {
+ // We are done immediately
+ console.warn("No conference handler or conference not started yet");
+ callback();
+ }
+ },
+ setVideoMute: function (mute, callback, options) {
+ if(!connection || !APP.RTC.localVideo)
+ return;
+
+ var localCallback = function (mute) {
+ connection.emuc.addVideoInfoToPresence(mute);
+ connection.emuc.sendPresence();
+ return callback(mute);
+ };
+
+ if (mute == APP.RTC.localVideo.isMuted())
+ {
+ // Even if no change occurs, the specified callback is to be executed.
+ // The specified callback may, optionally, return a successCallback
+ // which is to be executed as well.
+ var successCallback = localCallback(mute);
+
+ if (successCallback) {
+ successCallback();
+ }
+ } else {
+ APP.RTC.localVideo.setMute(!mute);
+ if(connection.jingle.activecall)
+ {
+ connection.jingle.activecall.setVideoMute(
+ mute, localCallback, options);
+ }
+ else {
+ localCallback(mute);
+ }
+
+ }
+ },
+ setAudioMute: function (mute, callback) {
+ if (!(connection && APP.RTC.localAudio)) {
+ return false;
+ }
+
+
+ if (this.forceMuted && !mute) {
+ console.info("Asking focus for unmute");
+ connection.moderate.setMute(connection.emuc.myroomjid, mute);
+ // FIXME: wait for result before resetting muted status
+ this.forceMuted = false;
+ }
+
+ if (mute == APP.RTC.localAudio.isMuted()) {
+ // Nothing to do
+ return true;
+ }
+
+ // It is not clear what is the right way to handle multiple tracks.
+ // So at least make sure that they are all muted or all unmuted and
+ // that we send presence just once.
+ APP.RTC.localAudio.mute();
+ // isMuted is the opposite of audioEnabled
+ connection.emuc.addAudioInfoToPresence(mute);
+ connection.emuc.sendPresence();
+ callback();
+ return true;
+ },
+ // Really mute video, i.e. dont even send black frames
+ muteVideo: function (pc, unmute) {
+ // FIXME: this probably needs another of those lovely state safeguards...
+ // which checks for iceconn == connected and sigstate == stable
+ pc.setRemoteDescription(pc.remoteDescription,
+ function () {
+ pc.createAnswer(
+ function (answer) {
+ var sdp = new SDP(answer.sdp);
+ if (sdp.media.length > 1) {
+ if (unmute)
+ sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
+ else
+ sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
+ sdp.raw = sdp.session + sdp.media.join('');
+ answer.sdp = sdp.raw;
+ }
+ pc.setLocalDescription(answer,
+ function () {
+ console.log('mute SLD ok');
+ },
+ function (error) {
+ console.log('mute SLD error');
+ APP.UI.messageHandler.showError("dialog.error",
+ "dialog.SLDFailure");
+ }
+ );
+ },
+ function (error) {
+ console.log(error);
+ APP.UI.messageHandler.showError();
+ }
+ );
+ },
+ function (error) {
+ console.log('muteVideo SRD error');
+ APP.UI.messageHandler.showError("dialog.error",
+ "dialog.SRDFailure");
+
+ }
+ );
+ },
+ toggleRecording: function (tokenEmptyCallback,
+ startingCallback, startedCallback) {
+ Recording.toggleRecording(tokenEmptyCallback,
+ startingCallback, startedCallback, connection);
+ },
+ addToPresence: function (name, value, dontSend) {
+ switch (name)
+ {
+ case "displayName":
+ connection.emuc.addDisplayNameToPresence(value);
+ break;
+ case "etherpad":
+ connection.emuc.addEtherpadToPresence(value);
+ break;
+ case "prezi":
+ connection.emuc.addPreziToPresence(value, 0);
+ break;
+ case "preziSlide":
+ connection.emuc.addCurrentSlideToPresence(value);
+ break;
+ case "connectionQuality":
+ connection.emuc.addConnectionInfoToPresence(value);
+ break;
+ case "email":
+ connection.emuc.addEmailToPresence(value);
+ default :
+ console.log("Unknown tag for presence.");
+ return;
+ }
+ if(!dontSend)
+ connection.emuc.sendPresence();
+ },
+ /**
+ * Sends 'data' as a log message to the focus. Returns true iff a message
+ * was sent.
+ * @param data
+ * @returns {boolean} true iff a message was sent.
+ */
+ sendLogs: function (data) {
+ if(!connection.emuc.focusMucJid)
+ return false;
+
+ var deflate = true;
+
+ var content = JSON.stringify(data);
+ if (deflate) {
+ content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
+ }
+ content = Base64.encode(content);
+ // XEP-0337-ish
+ var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
+ message.c('log', { xmlns: 'urn:xmpp:eventlog',
+ id: 'PeerConnectionStats'});
+ message.c('message').t(content).up();
+ if (deflate) {
+ message.c('tag', {name: "deflated", value: "true"}).up();
+ }
+ message.up();
+
+ connection.send(message);
+ return true;
+ },
+ populateData: function () {
+ var data = {};
+ if (connection.jingle) {
+ data = connection.jingle.populateData();
+ }
+ return data;
+ },
+ getLogger: function () {
+ if(connection.logger)
+ return connection.logger.log;
+ return null;
+ },
+ getPrezi: function () {
+ return connection.emuc.getPrezi(this.myJid());
+ },
+ removePreziFromPresence: function () {
+ connection.emuc.removePreziFromPresence();
+ connection.emuc.sendPresence();
+ },
+ sendChatMessage: function (message, nickname) {
+ connection.emuc.sendMessage(message, nickname);
+ },
+ setSubject: function (topic) {
+ connection.emuc.setSubject(topic);
+ },
+ lockRoom: function (key, onSuccess, onError, onNotSupported) {
+ connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
+ },
+ dial: function (to, from, roomName,roomPass) {
+ connection.rayo.dial(to, from, roomName,roomPass);
+ },
+ setMute: function (jid, mute) {
+ connection.moderate.setMute(jid, mute);
+ },
+ eject: function (jid) {
+ connection.moderate.eject(jid);
+ },
+ logout: function (callback) {
+ Moderator.logout(callback);
+ },
+ findJidFromResource: function (resource) {
+ return connection.emuc.findJidFromResource(resource);
+ },
+ getMembers: function () {
+ return connection.emuc.members;
+ },
+ getJidFromSSRC: function (ssrc) {
+ if(!connection)
+ return null;
+ return connection.emuc.ssrc2jid[ssrc];
+ },
+ getMUCJoined: function () {
+ return connection.emuc.joined;
+ },
+ getSessions: function () {
+ return connection.jingle.sessions;
+ }
+
+};
+
+module.exports = XMPP;
-function connect(jid, password) {
- connection = XMPP.createConnection();
- Moderator.setConnection(connection);
-
- if (connection.disco) {
- // for chrome, add multistream cap
- }
- connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
- if (config.useIPv6) {
- // https://code.google.com/p/webrtc/issues/detail?id=2828
- if (!connection.jingle.pc_constraints.optional)
- connection.jingle.pc_constraints.optional = [];
- connection.jingle.pc_constraints.optional.push({googIPv6: true});
- }
-
- var anonymousConnectionFailed = false;
- connection.connect(jid, password, function (status, msg) {
- console.log('Strophe status changed to',
- Strophe.getStatusString(status));
- if (status === Strophe.Status.CONNECTED) {
- if (config.useStunTurn) {
- connection.jingle.getStunAndTurnCredentials();
- }
-
- console.info("My Jabber ID: " + connection.jid);
-
- if(password)
- authenticatedUser = true;
- maybeDoJoin();
- } else if (status === Strophe.Status.CONNFAIL) {
- if(msg === 'x-strophe-bad-non-anon-jid') {
- anonymousConnectionFailed = true;
- }
- } else if (status === Strophe.Status.DISCONNECTED) {
- if(anonymousConnectionFailed) {
- // prompt user for username and password
- XMPP.promptLogin();
- }
- } else if (status === Strophe.Status.AUTHFAIL) {
- // wrong password or username, prompt user
- XMPP.promptLogin();
-
- }
- });
-}
-
-
-
-function maybeDoJoin() {
- if (connection && connection.connected &&
- Strophe.getResourceFromJid(connection.jid)
- && (APP.RTC.localAudio || APP.RTC.localVideo)) {
- // .connected is true while connecting?
- doJoin();
- }
-}
-
-function doJoin() {
- var roomName = APP.UI.generateRoomName();
-
- Moderator.allocateConferenceFocus(
- roomName, APP.UI.checkForNicknameAndJoin);
-}
-
-function initStrophePlugins()
-{
- require("./strophe.emuc")(XMPP, eventEmitter);
- require("./strophe.jingle")(XMPP, eventEmitter);
- require("./strophe.moderate")(XMPP);
- require("./strophe.util")();
- require("./strophe.rayo")();
- require("./strophe.logger")();
-}
-
-function registerListeners() {
- APP.RTC.addStreamListener(maybeDoJoin,
- StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
- APP.UI.addListener(UIEvents.NICKNAME_CHANGED, function (nickname) {
- XMPP.addToPresence("displayName", nickname);
- });
-}
-
-function setupEvents() {
- $(window).bind('beforeunload', function () {
- if (connection && connection.connected) {
- // ensure signout
- $.ajax({
- type: 'POST',
- url: config.bosh,
- async: false,
- cache: false,
- contentType: 'application/xml',
- data: "" +
- " " +
- "",
- success: function (data) {
- console.log('signed out');
- console.log(data);
- },
- error: function (XMLHttpRequest, textStatus, errorThrown) {
- console.log('signout error',
- textStatus + ' (' + errorThrown + ')');
- }
- });
- }
- XMPP.disposeConference(true);
- });
-}
-
-var XMPP = {
- sessionTerminated: false,
-
- /**
- * XMPP connection status
- */
- Status: Strophe.Status,
-
- /**
- * Remembers if we were muted by the focus.
- * @type {boolean}
- */
- forceMuted: false,
- start: function () {
- setupEvents();
- initStrophePlugins();
- registerListeners();
- Moderator.init(this, eventEmitter);
- var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
- // Force authenticated domain if room is appended with '?login=true'
- if (config.hosts.anonymousdomain &&
- window.location.search.indexOf("login=true") !== -1) {
- configDomain = config.hosts.domain;
- }
- var jid = configDomain || window.location.hostname;
- connect(jid, null);
- },
- createConnection: function () {
- var bosh = config.bosh || '/http-bind';
-
- return new Strophe.Connection(bosh);
- },
- getStatusString: function (status) {
- return Strophe.getStatusString(status);
- },
- promptLogin: function () {
- // FIXME: re-use LoginDialog which supports retries
- APP.UI.showLoginPopup(connect);
- },
- joinRoom: function(roomName, useNicks, nick)
- {
- var roomjid;
- roomjid = roomName;
-
- if (useNicks) {
- if (nick) {
- roomjid += '/' + nick;
- } else {
- roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
- }
- } else {
-
- var tmpJid = Strophe.getNodeFromJid(connection.jid);
-
- if(!authenticatedUser)
- tmpJid = tmpJid.substr(0, 8);
-
- roomjid += '/' + tmpJid;
- }
- connection.emuc.doJoin(roomjid);
- },
- myJid: function () {
- if(!connection)
- return null;
- return connection.emuc.myroomjid;
- },
- myResource: function () {
- if(!connection || ! connection.emuc.myroomjid)
- return null;
- return Strophe.getResourceFromJid(connection.emuc.myroomjid);
- },
- disposeConference: function (onUnload) {
- eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
- var handler = connection.jingle.activecall;
- if (handler && handler.peerconnection) {
- // FIXME: probably removing streams is not required and close() should
- // be enough
- if (APP.RTC.localAudio) {
- handler.peerconnection.removeStream(APP.RTC.localAudio.getOriginalStream(), onUnload);
- }
- if (APP.RTC.localVideo) {
- handler.peerconnection.removeStream(APP.RTC.localVideo.getOriginalStream(), onUnload);
- }
- handler.peerconnection.close();
- }
- connection.jingle.activecall = null;
- if(!onUnload)
- {
- this.sessionTerminated = true;
- connection.emuc.doLeave();
- }
- },
- addListener: function(type, listener)
- {
- eventEmitter.on(type, listener);
- },
- removeListener: function (type, listener) {
- eventEmitter.removeListener(type, listener);
- },
- allocateConferenceFocus: function(roomName, callback) {
- Moderator.allocateConferenceFocus(roomName, callback);
- },
- getLoginUrl: function (roomName, callback) {
- Moderator.getLoginUrl(roomName, callback);
- },
- getPopupLoginUrl: function (roomName, callback) {
- Moderator.getPopupLoginUrl(roomName, callback);
- },
- isModerator: function () {
- return Moderator.isModerator();
- },
- isSipGatewayEnabled: function () {
- return Moderator.isSipGatewayEnabled();
- },
- isExternalAuthEnabled: function () {
- return Moderator.isExternalAuthEnabled();
- },
- switchStreams: function (stream, oldStream, callback) {
- if (connection && connection.jingle.activecall) {
- // FIXME: will block switchInProgress on true value in case of exception
- connection.jingle.activecall.switchStreams(stream, oldStream, callback);
- } else {
- // We are done immediately
- console.warn("No conference handler or conference not started yet");
- callback();
- }
- },
- setVideoMute: function (mute, callback, options) {
- if(!connection || !APP.RTC.localVideo)
- return;
-
- var localCallback = function (mute) {
- connection.emuc.addVideoInfoToPresence(mute);
- connection.emuc.sendPresence();
- return callback(mute);
- };
-
- if (mute == APP.RTC.localVideo.isMuted())
- {
- // Even if no change occurs, the specified callback is to be executed.
- // The specified callback may, optionally, return a successCallback
- // which is to be executed as well.
- var successCallback = localCallback(mute);
-
- if (successCallback) {
- successCallback();
- }
- } else {
- APP.RTC.localVideo.setMute(!mute);
- if(connection.jingle.activecall)
- {
- connection.jingle.activecall.setVideoMute(
- mute, localCallback, options);
- }
- else {
- localCallback(mute);
- }
-
- }
- },
- setAudioMute: function (mute, callback) {
- if (!(connection && APP.RTC.localAudio)) {
- return false;
- }
-
-
- if (this.forceMuted && !mute) {
- console.info("Asking focus for unmute");
- connection.moderate.setMute(connection.emuc.myroomjid, mute);
- // FIXME: wait for result before resetting muted status
- this.forceMuted = false;
- }
-
- if (mute == APP.RTC.localAudio.isMuted()) {
- // Nothing to do
- return true;
- }
-
- // It is not clear what is the right way to handle multiple tracks.
- // So at least make sure that they are all muted or all unmuted and
- // that we send presence just once.
- APP.RTC.localAudio.mute();
- // isMuted is the opposite of audioEnabled
- connection.emuc.addAudioInfoToPresence(mute);
- connection.emuc.sendPresence();
- callback();
- return true;
- },
- // Really mute video, i.e. dont even send black frames
- muteVideo: function (pc, unmute) {
- // FIXME: this probably needs another of those lovely state safeguards...
- // which checks for iceconn == connected and sigstate == stable
- pc.setRemoteDescription(pc.remoteDescription,
- function () {
- pc.createAnswer(
- function (answer) {
- var sdp = new SDP(answer.sdp);
- if (sdp.media.length > 1) {
- if (unmute)
- sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
- else
- sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
- sdp.raw = sdp.session + sdp.media.join('');
- answer.sdp = sdp.raw;
- }
- pc.setLocalDescription(answer,
- function () {
- console.log('mute SLD ok');
- },
- function (error) {
- console.log('mute SLD error');
- APP.UI.messageHandler.showError("dialog.error",
- "dialog.SLDFailure");
- }
- );
- },
- function (error) {
- console.log(error);
- APP.UI.messageHandler.showError();
- }
- );
- },
- function (error) {
- console.log('muteVideo SRD error');
- APP.UI.messageHandler.showError("dialog.error",
- "dialog.SRDFailure");
-
- }
- );
- },
- toggleRecording: function (tokenEmptyCallback,
- startingCallback, startedCallback) {
- Recording.toggleRecording(tokenEmptyCallback,
- startingCallback, startedCallback, connection);
- },
- addToPresence: function (name, value, dontSend) {
- switch (name)
- {
- case "displayName":
- connection.emuc.addDisplayNameToPresence(value);
- break;
- case "etherpad":
- connection.emuc.addEtherpadToPresence(value);
- break;
- case "prezi":
- connection.emuc.addPreziToPresence(value, 0);
- break;
- case "preziSlide":
- connection.emuc.addCurrentSlideToPresence(value);
- break;
- case "connectionQuality":
- connection.emuc.addConnectionInfoToPresence(value);
- break;
- case "email":
- connection.emuc.addEmailToPresence(value);
- default :
- console.log("Unknown tag for presence.");
- return;
- }
- if(!dontSend)
- connection.emuc.sendPresence();
- },
- /**
- * Sends 'data' as a log message to the focus. Returns true iff a message
- * was sent.
- * @param data
- * @returns {boolean} true iff a message was sent.
- */
- sendLogs: function (data) {
- if(!connection.emuc.focusMucJid)
- return false;
-
- var deflate = true;
-
- var content = JSON.stringify(data);
- if (deflate) {
- content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
- }
- content = Base64.encode(content);
- // XEP-0337-ish
- var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
- message.c('log', { xmlns: 'urn:xmpp:eventlog',
- id: 'PeerConnectionStats'});
- message.c('message').t(content).up();
- if (deflate) {
- message.c('tag', {name: "deflated", value: "true"}).up();
- }
- message.up();
-
- connection.send(message);
- return true;
- },
- populateData: function () {
- var data = {};
- if (connection.jingle) {
- data = connection.jingle.populateData();
- }
- return data;
- },
- getLogger: function () {
- if(connection.logger)
- return connection.logger.log;
- return null;
- },
- getPrezi: function () {
- return connection.emuc.getPrezi(this.myJid());
- },
- removePreziFromPresence: function () {
- connection.emuc.removePreziFromPresence();
- connection.emuc.sendPresence();
- },
- sendChatMessage: function (message, nickname) {
- connection.emuc.sendMessage(message, nickname);
- },
- setSubject: function (topic) {
- connection.emuc.setSubject(topic);
- },
- lockRoom: function (key, onSuccess, onError, onNotSupported) {
- connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
- },
- dial: function (to, from, roomName,roomPass) {
- connection.rayo.dial(to, from, roomName,roomPass);
- },
- setMute: function (jid, mute) {
- connection.moderate.setMute(jid, mute);
- },
- eject: function (jid) {
- connection.moderate.eject(jid);
- },
- logout: function (callback) {
- Moderator.logout(callback);
- },
- findJidFromResource: function (resource) {
- return connection.emuc.findJidFromResource(resource);
- },
- getMembers: function () {
- return connection.emuc.members;
- },
- getJidFromSSRC: function (ssrc) {
- if(!connection)
- return null;
- return connection.emuc.ssrc2jid[ssrc];
- },
- getMUCJoined: function () {
- return connection.emuc.joined;
- },
- getSessions: function () {
- return connection.jingle.sessions;
- }
-
-};
-
-module.exports = XMPP;
-
-},{"../../service/RTC/StreamEventTypes":91,"../../service/UI/UIEvents":92,"../../service/xmpp/XMPPEvents":97,"./SDP":49,"./moderator":53,"./recording":54,"./strophe.emuc":55,"./strophe.jingle":56,"./strophe.logger":57,"./strophe.moderate":58,"./strophe.rayo":59,"./strophe.util":60,"events":98,"pako":63}],62:[function(require,module,exports){
+},{"../../service/RTC/StreamEventTypes":92,"../../service/UI/UIEvents":93,"../../service/xmpp/XMPPEvents":98,"./SDP":50,"./moderator":54,"./recording":55,"./strophe.emuc":56,"./strophe.jingle":57,"./strophe.logger":58,"./strophe.moderate":59,"./strophe.rayo":60,"./strophe.util":61,"events":1,"pako":64}],63:[function(require,module,exports){
// i18next, v1.7.7
// Copyright (c)2014 Jan Mühlemann (jamuhl).
// Distributed under MIT license
@@ -19259,7 +19598,7 @@ module.exports = XMPP;
i18n.options = o;
})();
-},{"jquery":"jquery"}],63:[function(require,module,exports){
+},{"jquery":"jquery"}],64:[function(require,module,exports){
// Top level file is just a mixin of submodules & constants
'use strict';
@@ -19274,7 +19613,7 @@ var pako = {};
assign(pako, deflate, inflate, constants);
module.exports = pako;
-},{"./lib/deflate":64,"./lib/inflate":65,"./lib/utils/common":66,"./lib/zlib/constants":69}],64:[function(require,module,exports){
+},{"./lib/deflate":65,"./lib/inflate":66,"./lib/utils/common":67,"./lib/zlib/constants":70}],65:[function(require,module,exports){
'use strict';
@@ -19636,7 +19975,7 @@ exports.Deflate = Deflate;
exports.deflate = deflate;
exports.deflateRaw = deflateRaw;
exports.gzip = gzip;
-},{"./utils/common":66,"./utils/strings":67,"./zlib/deflate.js":71,"./zlib/messages":76,"./zlib/zstream":78}],65:[function(require,module,exports){
+},{"./utils/common":67,"./utils/strings":68,"./zlib/deflate.js":72,"./zlib/messages":77,"./zlib/zstream":79}],66:[function(require,module,exports){
'use strict';
@@ -20002,7 +20341,7 @@ exports.inflate = inflate;
exports.inflateRaw = inflateRaw;
exports.ungzip = inflate;
-},{"./utils/common":66,"./utils/strings":67,"./zlib/constants":69,"./zlib/gzheader":72,"./zlib/inflate.js":74,"./zlib/messages":76,"./zlib/zstream":78}],66:[function(require,module,exports){
+},{"./utils/common":67,"./utils/strings":68,"./zlib/constants":70,"./zlib/gzheader":73,"./zlib/inflate.js":75,"./zlib/messages":77,"./zlib/zstream":79}],67:[function(require,module,exports){
'use strict';
@@ -20105,7 +20444,7 @@ exports.setTyped = function (on) {
};
exports.setTyped(TYPED_OK);
-},{}],67:[function(require,module,exports){
+},{}],68:[function(require,module,exports){
// String encode/decode helpers
'use strict';
@@ -20292,7 +20631,7 @@ exports.utf8border = function(buf, max) {
return (pos + _utf8len[buf[pos]] > max) ? pos : max;
};
-},{"./common":66}],68:[function(require,module,exports){
+},{"./common":67}],69:[function(require,module,exports){
'use strict';
// Note: adler32 takes 12% for level 0 and 2% for level 6.
@@ -20325,7 +20664,7 @@ function adler32(adler, buf, len, pos) {
module.exports = adler32;
-},{}],69:[function(require,module,exports){
+},{}],70:[function(require,module,exports){
module.exports = {
/* Allowed flush values; see deflate() and inflate() below for details */
@@ -20373,7 +20712,7 @@ module.exports = {
Z_DEFLATED: 8
//Z_NULL: null // Use -1 or null inline, depending on var type
};
-},{}],70:[function(require,module,exports){
+},{}],71:[function(require,module,exports){
'use strict';
// Note: we can't get significant speed boost here.
@@ -20415,7 +20754,7 @@ function crc32(crc, buf, len, pos) {
module.exports = crc32;
-},{}],71:[function(require,module,exports){
+},{}],72:[function(require,module,exports){
'use strict';
var utils = require('../utils/common');
@@ -22181,7 +22520,7 @@ exports.deflatePending = deflatePending;
exports.deflatePrime = deflatePrime;
exports.deflateTune = deflateTune;
*/
-},{"../utils/common":66,"./adler32":68,"./crc32":70,"./messages":76,"./trees":77}],72:[function(require,module,exports){
+},{"../utils/common":67,"./adler32":69,"./crc32":71,"./messages":77,"./trees":78}],73:[function(require,module,exports){
'use strict';
@@ -22222,7 +22561,7 @@ function GZheader() {
}
module.exports = GZheader;
-},{}],73:[function(require,module,exports){
+},{}],74:[function(require,module,exports){
'use strict';
// See state defs from inflate.js
@@ -22549,7 +22888,7 @@ module.exports = function inflate_fast(strm, start) {
return;
};
-},{}],74:[function(require,module,exports){
+},{}],75:[function(require,module,exports){
'use strict';
@@ -24053,7 +24392,7 @@ exports.inflateSync = inflateSync;
exports.inflateSyncPoint = inflateSyncPoint;
exports.inflateUndermine = inflateUndermine;
*/
-},{"../utils/common":66,"./adler32":68,"./crc32":70,"./inffast":73,"./inftrees":75}],75:[function(require,module,exports){
+},{"../utils/common":67,"./adler32":69,"./crc32":71,"./inffast":74,"./inftrees":76}],76:[function(require,module,exports){
'use strict';
@@ -24380,7 +24719,7 @@ module.exports = function inflate_table(type, lens, lens_index, codes, table, ta
return 0;
};
-},{"../utils/common":66}],76:[function(require,module,exports){
+},{"../utils/common":67}],77:[function(require,module,exports){
'use strict';
module.exports = {
@@ -24394,7 +24733,7 @@ module.exports = {
'-5': 'buffer error', /* Z_BUF_ERROR (-5) */
'-6': 'incompatible version' /* Z_VERSION_ERROR (-6) */
};
-},{}],77:[function(require,module,exports){
+},{}],78:[function(require,module,exports){
'use strict';
@@ -25594,7 +25933,7 @@ exports._tr_stored_block = _tr_stored_block;
exports._tr_flush_block = _tr_flush_block;
exports._tr_tally = _tr_tally;
exports._tr_align = _tr_align;
-},{"../utils/common":66}],78:[function(require,module,exports){
+},{"../utils/common":67}],79:[function(require,module,exports){
'use strict';
@@ -25624,658 +25963,658 @@ function ZStream() {
}
module.exports = ZStream;
-},{}],79:[function(require,module,exports){
-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;
-}
-
-
},{}],80:[function(require,module,exports){
-exports.Interop = require('./interop');
-
-},{"./interop":81}],81:[function(require,module,exports){
-var transform = require('./transform');
-var arrayEquals = require('./array-equals');
-
-function Interop() { }
-module.exports = Interop;
-
-/**
- * This map holds the most recent Plan A 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 {{}}
- */
-var cache = {};
-
-/**
- * This method transforms a Plan A 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) {
-
- //#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 Plan A 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 A.');
- return desc;
- }
-
- //#endregion
-
- // Plan A SDP is our "precious". Cache it for later use in the Plan B ->
- // Plan A transformation.
- cache[desc.type] = desc.sdp;
-
- //#region Convert from Plan A 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. channels['audio']->channel
- // obj.
- var channels = {};
-
- // Used to build the group:BUNDLE value after the channels construction
- // loop.
- var types = [];
-
- // Implode the Plan A m-lines/tracks into Plan B "channels".
- media.forEach(function(mLine) {
-
- // rtcp-mux is required in the Plan B SDP.
- if (typeof mLine.rtcpMux !== 'string' ||
- mLine.rtcpMux !== 'rtcp-mux') {
- 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 mLine.type, then use this mLine
- // as the channel basis.
- if (typeof channels[mLine.type] === 'undefined') {
- channels[mLine.type] = mLine;
- }
-
- // Add sources to the channel and handle a=msid.
- if (typeof mLine.sources === 'object') {
- Object.keys(mLine.sources).forEach(function(ssrc) {
- // Assign the sources to the channel.
- channels[mLine.type].sources[ssrc] = mLine.sources[ssrc];
-
- // In Plan B the msid is an SSRC attribute. Also, we don't care
- // about the obsolete label and mslabel attributes.
- channels[mLine.type].sources[ssrc].msid = mLine.msid;
-
- // NOTE ssrcs in ssrc groups will share msids, as
- // draft-uberti-rtcweb-plan-00 mandates.
- });
- }
-
- // Add ssrc groups to the channel.
- if (typeof mLine.ssrcGroups !== 'undefined' &&
- Array.isArray(mLine.ssrcGroups)) {
-
- // Create the ssrcGroups array, if it's not defined.
- if (typeof channel.ssrcGroups === 'undefined' ||
- !Array.isArray(channel.ssrcGroups)) {
- channel.ssrcGroups = [];
- }
-
- channel.ssrcGroups = channel.ssrcGroups.concat(mLine.ssrcGroups);
- }
-
- if (channels[mLine.type] === mLine) {
- // Copy ICE related stuff from the principal media line.
- mLine.candidates = media[0].candidates;
- mLine.iceUfrag = media[0].iceUfrag;
- mLine.icePwd = media[0].icePwd;
- mLine.fingerprint = media[0].fingerprint;
-
- // Plan B mids are in ['audio', 'video', 'data']
- mLine.mid = mLine.type;
-
- // Plan B doesn't support/need the bundle-only attribute.
- delete mLine.bundleOnly;
-
- // In Plan B the msid is an SSRC attribute.
- delete mLine.msid;
-
- // Used to build the group:BUNDLE value after this loop.
- types.push(mLine.type);
-
- // Add the channel to the new media array.
- session.media.push(mLine);
- }
- });
-
- // We regenerate the BUNDLE group with the new mids.
- session.groups.every(function(group) {
- if (group.type === 'BUNDLE') {
- group.mids = types.join(' ');
- return false;
- } else {
- return true;
- }
- });
-
- // msid semantic
- session.msidSemantic = {
- semantic: 'WMS',
- token: '*'
- };
-
- var resStr = transform.write(session);
-
- return new RTCSessionDescription({
- type: desc.type,
- sdp: resStr
- });
-
- //#endregion
-};
-
-/**
- * This method transforms a Plan B SDP to an equivalent Plan A SDP. A
- * PeerConnection wrapper transforms the SDP to Plan A before passing it to FF.
- *
- * @param desc
- * @returns {*}
- */
-Interop.prototype.toPlanA = function(desc) {
-
- //#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 Plan A 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) {
- throw new Error("Cannot convert to Plan A because m-lines that are " +
- "not bundled were found.");
- }
-
- //#endregion
-
-
- //#region Convert from Plan B to Plan A.
-
- // Unfortunately, a Plan B offer/answer doesn't have enough information to
- // rebuild an equivalent Plan A offer/answer.
- //
- // For example, if this is a local answer (in Plan A 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
- // Plan A), 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 Plan A (if it exists)
- // to assign mids to ssrcs.
-
- var cached;
- if (typeof cache[desc.type] !== 'undefined') {
- cached = transform.parse(cache[desc.type]);
- }
-
- // A helper map that sends mids to m-line objects. We use it later to
- // rebuild the Plan A style session.media array.
- var media = {};
- session.media.forEach(function(channel) {
- if (typeof channel.rtcpMux !== 'string' ||
- channel.rtcpMux !== 'rtcp-mux') {
- throw new Error("Cannot convert to Plan A because m-lines " +
- "without the rtcp-mux attribute were found.");
- }
-
- // With rtcp-mux and bundle all the channels should have the same ICE
- // stuff.
- var sources = channel.sources;
- var ssrcGroups = channel.ssrcGroups;
- var candidates = channel.candidates;
- var iceUfrag = channel.iceUfrag;
- var icePwd = channel.icePwd;
- var fingerprint = channel.fingerprint;
- var port = channel.port;
-
- // We'll use the "channel" object as a prototype for each new "mLine"
- // that we create, but first we need to clean it up a bit.
- delete channel.sources;
- delete channel.ssrcGroups;
- delete channel.candidates;
- delete channel.iceUfrag;
- delete channel.icePwd;
- delete channel.fingerprint;
- delete channel.port;
- delete channel.mid;
-
- // inverted ssrc group map
- var invertedGroups = {};
- if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
- ssrcGroups.forEach(function (ssrcGroup) {
-
- // TODO(gp) find out how to receive simulcast with FF. For the
- // time being, hide it.
- if (ssrcGroup.semantics === 'SIM') {
- return;
- }
-
- if (typeof ssrcGroup.ssrcs !== 'undefined' &&
- Array.isArray(ssrcGroup.ssrcs)) {
- ssrcGroup.ssrcs.forEach(function (ssrc) {
- if (typeof invertedGroups[ssrc] === 'undefined') {
- invertedGroups[ssrc] = [];
- }
-
- invertedGroups[ssrc].push(ssrcGroup);
- });
- }
- });
- }
-
- // ssrc to m-line index.
- var mLines = {};
-
- if (typeof sources === 'object') {
-
- // Explode the Plan B channel sources with one m-line per source.
- Object.keys(sources).forEach(function(ssrc) {
-
- var mLine;
- if (typeof invertedGroups[ssrc] !== 'undefined' &&
- Array.isArray(invertedGroups[ssrc])) {
- invertedGroups[ssrc].every(function (ssrcGroup) {
- // ssrcGroup.ssrcs *is* an Array, no need to check
- // again here.
- return ssrcGroup.ssrcs.every(function (related) {
- if (typeof mLines[related] === 'object') {
- mLine = mLines[related];
- return false;
- } else {
- return true;
- }
- });
- });
- }
-
- if (typeof mLine === 'object') {
- // the m-line already exists. Just add the source.
- mLine.sources[ssrc] = sources[ssrc];
- delete sources[ssrc].msid;
- } else {
- // Use the "channel" as a prototype for the "mLine".
- mLine = Object.create(channel);
- mLines[ssrc] = mLine;
-
- // Assign the msid of the source to the m-line.
- mLine.msid = sources[ssrc].msid;
- delete sources[ssrc].msid;
-
- // We assign one SSRC per media line.
- mLine.sources = {};
- mLine.sources[ssrc] = sources[ssrc];
- mLine.ssrcGroups = invertedGroups[ssrc];
-
- // Use the cached Plan A 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) {
- mLine.mid = m.mid;
- }
- });
- }
- });
- }
-
- if (typeof mLine.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 Plan A style, we MUST
- // already have a cached answer with all the local SSRCs
- // mapped to some mLine/mid.
-
- if (desc.type === 'answer') {
- throw new Error("An unmapped SSRC was found.");
- }
-
- mLine.mid = [channel.type, '-', ssrc].join('');
- }
-
- // Include the candidates in the 1st media line.
- mLine.candidates = candidates;
- mLine.iceUfrag = iceUfrag;
- mLine.icePwd = icePwd;
- mLine.fingerprint = fingerprint;
- mLine.port = port;
-
- media[mLine.mid] = mLine;
- }
- });
- }
- });
-
- // 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 use the cached offer to
- // find the m-lines that are missing (from the converted answer), and
- // use the cached answer to complete the converted answer.
-
- if (typeof cache['offer'] === 'undefined') {
- throw new Error("An answer is being processed but we couldn't " +
- "find a cached offer.");
- }
-
- var cachedOffer = transform.parse(cache['offer']);
-
- if (typeof cachedOffer === 'undefined' ||
- typeof cachedOffer.media === 'undefined' ||
- !Array.isArray(cachedOffer.media)) {
- // FIXME(gp) is this really a problem in the general case?
- throw new Error("The cached offer has no media.");
- }
-
- cachedOffer.media.forEach(function(mo) {
-
- var mLine;
- if (typeof media[mo.mid] === 'undefined') {
-
- // This is probably an m-line containing a remote track only.
- // It MUST exist in the cached answer as a remote track only
- // mLine.
-
- cached.media.every(function(ma) {
- if (mo.mid == ma.mid) {
- mLine = ma;
- return false;
- } else {
- return true;
- }
- });
- } else {
- mLine = media[mo.mid];
- }
-
- if (typeof mLine === 'undefined') {
- throw new Error("The cached offer contains an m-line that " +
- "doesn't exist neither in the cached answer nor in " +
- "the converted answer.");
- }
-
- session.media.push(mLine);
- mids.push(mLine.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=inacive.
-
- if (typeof cached !== 'undefined' &&
- typeof cached.media !== 'undefined' &&
- Array.isArray(cached.media)) {
- cached.media.forEach(function(pm) {
- mids.push(pm.mid);
- if (typeof media[pm.mid] !== 'undefined') {
- session.media.push(media[pm.mid]);
- } else {
- delete pm.msid;
- delete pm.sources;
- delete pm.ssrcGroups;
- pm.direction = 'recvonly';
- session.media.push(pm);
- }
- });
- }
-
- // Add all the remaining (new) m-lines of the transformed SDP.
- Object.keys(media).forEach(function(mid) {
- if (mids.indexOf(mid) === -1) {
- mids.push(mid);
- session.media.push(media[mid]);
- }
- });
- }
-
- // We regenerate the BUNDLE group (since we regenerated the mids)
- session.groups.every(function(group) {
- if (group.type === 'BUNDLE') {
- group.mids = mids.join(' ');
- return false;
- } else {
- return true;
- }
- });
-
- // msid semantic
- session.msidSemantic = {
- semantic: 'WMS',
- token: '*'
- };
-
- var resStr = transform.write(session);
-
- // Cache the transformed SDP (Plan A) for later re-use in this function.
- cache[desc.type] = resStr;
-
- return new RTCSessionDescription({
- type: desc.type,
- sdp: resStr
- });
-
- //#endregion
-};
-
-},{"./array-equals":79,"./transform":82}],82:[function(require,module,exports){
-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":84}],83:[function(require,module,exports){
+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;
+}
+
+
+},{}],81:[function(require,module,exports){
+exports.Interop = require('./interop');
+
+},{"./interop":82}],82:[function(require,module,exports){
+var transform = require('./transform');
+var arrayEquals = require('./array-equals');
+
+function Interop() { }
+module.exports = Interop;
+
+/**
+ * This map holds the most recent Plan A 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 {{}}
+ */
+var cache = {};
+
+/**
+ * This method transforms a Plan A 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) {
+
+ //#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 Plan A 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 A.');
+ return desc;
+ }
+
+ //#endregion
+
+ // Plan A SDP is our "precious". Cache it for later use in the Plan B ->
+ // Plan A transformation.
+ cache[desc.type] = desc.sdp;
+
+ //#region Convert from Plan A 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. channels['audio']->channel
+ // obj.
+ var channels = {};
+
+ // Used to build the group:BUNDLE value after the channels construction
+ // loop.
+ var types = [];
+
+ // Implode the Plan A m-lines/tracks into Plan B "channels".
+ media.forEach(function(mLine) {
+
+ // rtcp-mux is required in the Plan B SDP.
+ if (typeof mLine.rtcpMux !== 'string' ||
+ mLine.rtcpMux !== 'rtcp-mux') {
+ 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 mLine.type, then use this mLine
+ // as the channel basis.
+ if (typeof channels[mLine.type] === 'undefined') {
+ channels[mLine.type] = mLine;
+ }
+
+ // Add sources to the channel and handle a=msid.
+ if (typeof mLine.sources === 'object') {
+ Object.keys(mLine.sources).forEach(function(ssrc) {
+ // Assign the sources to the channel.
+ channels[mLine.type].sources[ssrc] = mLine.sources[ssrc];
+
+ // In Plan B the msid is an SSRC attribute. Also, we don't care
+ // about the obsolete label and mslabel attributes.
+ channels[mLine.type].sources[ssrc].msid = mLine.msid;
+
+ // NOTE ssrcs in ssrc groups will share msids, as
+ // draft-uberti-rtcweb-plan-00 mandates.
+ });
+ }
+
+ // Add ssrc groups to the channel.
+ if (typeof mLine.ssrcGroups !== 'undefined' &&
+ Array.isArray(mLine.ssrcGroups)) {
+
+ // Create the ssrcGroups array, if it's not defined.
+ if (typeof channel.ssrcGroups === 'undefined' ||
+ !Array.isArray(channel.ssrcGroups)) {
+ channel.ssrcGroups = [];
+ }
+
+ channel.ssrcGroups = channel.ssrcGroups.concat(mLine.ssrcGroups);
+ }
+
+ if (channels[mLine.type] === mLine) {
+ // Copy ICE related stuff from the principal media line.
+ mLine.candidates = media[0].candidates;
+ mLine.iceUfrag = media[0].iceUfrag;
+ mLine.icePwd = media[0].icePwd;
+ mLine.fingerprint = media[0].fingerprint;
+
+ // Plan B mids are in ['audio', 'video', 'data']
+ mLine.mid = mLine.type;
+
+ // Plan B doesn't support/need the bundle-only attribute.
+ delete mLine.bundleOnly;
+
+ // In Plan B the msid is an SSRC attribute.
+ delete mLine.msid;
+
+ // Used to build the group:BUNDLE value after this loop.
+ types.push(mLine.type);
+
+ // Add the channel to the new media array.
+ session.media.push(mLine);
+ }
+ });
+
+ // We regenerate the BUNDLE group with the new mids.
+ session.groups.every(function(group) {
+ if (group.type === 'BUNDLE') {
+ group.mids = types.join(' ');
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ // msid semantic
+ session.msidSemantic = {
+ semantic: 'WMS',
+ token: '*'
+ };
+
+ var resStr = transform.write(session);
+
+ return new RTCSessionDescription({
+ type: desc.type,
+ sdp: resStr
+ });
+
+ //#endregion
+};
+
+/**
+ * This method transforms a Plan B SDP to an equivalent Plan A SDP. A
+ * PeerConnection wrapper transforms the SDP to Plan A before passing it to FF.
+ *
+ * @param desc
+ * @returns {*}
+ */
+Interop.prototype.toPlanA = function(desc) {
+
+ //#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 Plan A 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) {
+ throw new Error("Cannot convert to Plan A because m-lines that are " +
+ "not bundled were found.");
+ }
+
+ //#endregion
+
+
+ //#region Convert from Plan B to Plan A.
+
+ // Unfortunately, a Plan B offer/answer doesn't have enough information to
+ // rebuild an equivalent Plan A offer/answer.
+ //
+ // For example, if this is a local answer (in Plan A 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
+ // Plan A), 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 Plan A (if it exists)
+ // to assign mids to ssrcs.
+
+ var cached;
+ if (typeof cache[desc.type] !== 'undefined') {
+ cached = transform.parse(cache[desc.type]);
+ }
+
+ // A helper map that sends mids to m-line objects. We use it later to
+ // rebuild the Plan A style session.media array.
+ var media = {};
+ session.media.forEach(function(channel) {
+ if (typeof channel.rtcpMux !== 'string' ||
+ channel.rtcpMux !== 'rtcp-mux') {
+ throw new Error("Cannot convert to Plan A because m-lines " +
+ "without the rtcp-mux attribute were found.");
+ }
+
+ // With rtcp-mux and bundle all the channels should have the same ICE
+ // stuff.
+ var sources = channel.sources;
+ var ssrcGroups = channel.ssrcGroups;
+ var candidates = channel.candidates;
+ var iceUfrag = channel.iceUfrag;
+ var icePwd = channel.icePwd;
+ var fingerprint = channel.fingerprint;
+ var port = channel.port;
+
+ // We'll use the "channel" object as a prototype for each new "mLine"
+ // that we create, but first we need to clean it up a bit.
+ delete channel.sources;
+ delete channel.ssrcGroups;
+ delete channel.candidates;
+ delete channel.iceUfrag;
+ delete channel.icePwd;
+ delete channel.fingerprint;
+ delete channel.port;
+ delete channel.mid;
+
+ // inverted ssrc group map
+ var invertedGroups = {};
+ if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
+ ssrcGroups.forEach(function (ssrcGroup) {
+
+ // TODO(gp) find out how to receive simulcast with FF. For the
+ // time being, hide it.
+ if (ssrcGroup.semantics === 'SIM') {
+ return;
+ }
+
+ if (typeof ssrcGroup.ssrcs !== 'undefined' &&
+ Array.isArray(ssrcGroup.ssrcs)) {
+ ssrcGroup.ssrcs.forEach(function (ssrc) {
+ if (typeof invertedGroups[ssrc] === 'undefined') {
+ invertedGroups[ssrc] = [];
+ }
+
+ invertedGroups[ssrc].push(ssrcGroup);
+ });
+ }
+ });
+ }
+
+ // ssrc to m-line index.
+ var mLines = {};
+
+ if (typeof sources === 'object') {
+
+ // Explode the Plan B channel sources with one m-line per source.
+ Object.keys(sources).forEach(function(ssrc) {
+
+ var mLine;
+ if (typeof invertedGroups[ssrc] !== 'undefined' &&
+ Array.isArray(invertedGroups[ssrc])) {
+ invertedGroups[ssrc].every(function (ssrcGroup) {
+ // ssrcGroup.ssrcs *is* an Array, no need to check
+ // again here.
+ return ssrcGroup.ssrcs.every(function (related) {
+ if (typeof mLines[related] === 'object') {
+ mLine = mLines[related];
+ return false;
+ } else {
+ return true;
+ }
+ });
+ });
+ }
+
+ if (typeof mLine === 'object') {
+ // the m-line already exists. Just add the source.
+ mLine.sources[ssrc] = sources[ssrc];
+ delete sources[ssrc].msid;
+ } else {
+ // Use the "channel" as a prototype for the "mLine".
+ mLine = Object.create(channel);
+ mLines[ssrc] = mLine;
+
+ // Assign the msid of the source to the m-line.
+ mLine.msid = sources[ssrc].msid;
+ delete sources[ssrc].msid;
+
+ // We assign one SSRC per media line.
+ mLine.sources = {};
+ mLine.sources[ssrc] = sources[ssrc];
+ mLine.ssrcGroups = invertedGroups[ssrc];
+
+ // Use the cached Plan A 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) {
+ mLine.mid = m.mid;
+ }
+ });
+ }
+ });
+ }
+
+ if (typeof mLine.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 Plan A style, we MUST
+ // already have a cached answer with all the local SSRCs
+ // mapped to some mLine/mid.
+
+ if (desc.type === 'answer') {
+ throw new Error("An unmapped SSRC was found.");
+ }
+
+ mLine.mid = [channel.type, '-', ssrc].join('');
+ }
+
+ // Include the candidates in the 1st media line.
+ mLine.candidates = candidates;
+ mLine.iceUfrag = iceUfrag;
+ mLine.icePwd = icePwd;
+ mLine.fingerprint = fingerprint;
+ mLine.port = port;
+
+ media[mLine.mid] = mLine;
+ }
+ });
+ }
+ });
+
+ // 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 use the cached offer to
+ // find the m-lines that are missing (from the converted answer), and
+ // use the cached answer to complete the converted answer.
+
+ if (typeof cache['offer'] === 'undefined') {
+ throw new Error("An answer is being processed but we couldn't " +
+ "find a cached offer.");
+ }
+
+ var cachedOffer = transform.parse(cache['offer']);
+
+ if (typeof cachedOffer === 'undefined' ||
+ typeof cachedOffer.media === 'undefined' ||
+ !Array.isArray(cachedOffer.media)) {
+ // FIXME(gp) is this really a problem in the general case?
+ throw new Error("The cached offer has no media.");
+ }
+
+ cachedOffer.media.forEach(function(mo) {
+
+ var mLine;
+ if (typeof media[mo.mid] === 'undefined') {
+
+ // This is probably an m-line containing a remote track only.
+ // It MUST exist in the cached answer as a remote track only
+ // mLine.
+
+ cached.media.every(function(ma) {
+ if (mo.mid == ma.mid) {
+ mLine = ma;
+ return false;
+ } else {
+ return true;
+ }
+ });
+ } else {
+ mLine = media[mo.mid];
+ }
+
+ if (typeof mLine === 'undefined') {
+ throw new Error("The cached offer contains an m-line that " +
+ "doesn't exist neither in the cached answer nor in " +
+ "the converted answer.");
+ }
+
+ session.media.push(mLine);
+ mids.push(mLine.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=inacive.
+
+ if (typeof cached !== 'undefined' &&
+ typeof cached.media !== 'undefined' &&
+ Array.isArray(cached.media)) {
+ cached.media.forEach(function(pm) {
+ mids.push(pm.mid);
+ if (typeof media[pm.mid] !== 'undefined') {
+ session.media.push(media[pm.mid]);
+ } else {
+ delete pm.msid;
+ delete pm.sources;
+ delete pm.ssrcGroups;
+ pm.direction = 'recvonly';
+ session.media.push(pm);
+ }
+ });
+ }
+
+ // Add all the remaining (new) m-lines of the transformed SDP.
+ Object.keys(media).forEach(function(mid) {
+ if (mids.indexOf(mid) === -1) {
+ mids.push(mid);
+ session.media.push(media[mid]);
+ }
+ });
+ }
+
+ // We regenerate the BUNDLE group (since we regenerated the mids)
+ session.groups.every(function(group) {
+ if (group.type === 'BUNDLE') {
+ group.mids = mids.join(' ');
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ // msid semantic
+ session.msidSemantic = {
+ semantic: 'WMS',
+ token: '*'
+ };
+
+ var resStr = transform.write(session);
+
+ // Cache the transformed SDP (Plan A) for later re-use in this function.
+ cache[desc.type] = resStr;
+
+ return new RTCSessionDescription({
+ type: desc.type,
+ sdp: resStr
+ });
+
+ //#endregion
+};
+
+},{"./array-equals":80,"./transform":83}],83:[function(require,module,exports){
+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":85}],84:[function(require,module,exports){
var grammar = module.exports = {
v: [{
name: 'version',
@@ -26520,7 +26859,7 @@ Object.keys(grammar).forEach(function (key) {
});
});
-},{}],84:[function(require,module,exports){
+},{}],85:[function(require,module,exports){
var parser = require('./parser');
var writer = require('./writer');
@@ -26530,7 +26869,7 @@ exports.parseFmtpConfig = parser.parseFmtpConfig;
exports.parsePayloads = parser.parsePayloads;
exports.parseRemoteCandidates = parser.parseRemoteCandidates;
-},{"./parser":85,"./writer":86}],85:[function(require,module,exports){
+},{"./parser":86,"./writer":87}],86:[function(require,module,exports){
var toIntIfInt = function (v) {
return String(Number(v)) === v ? Number(v) : v;
};
@@ -26625,7 +26964,7 @@ exports.parseRemoteCandidates = function (str) {
return candidates;
};
-},{"./grammar":83}],86:[function(require,module,exports){
+},{"./grammar":84}],87:[function(require,module,exports){
var grammar = require('./grammar');
// customized util.format - discards excess arguments and can void middle ones
@@ -26741,490 +27080,187 @@ module.exports = function (session, opts) {
return sdp.join('\r\n') + '\r\n';
};
-},{"./grammar":83}],87:[function(require,module,exports){
-var MediaStreamType = {
- VIDEO_TYPE: "Video",
-
- AUDIO_TYPE: "Audio"
-};
+},{"./grammar":84}],88:[function(require,module,exports){
+var MediaStreamType = {
+ VIDEO_TYPE: "Video",
+
+ AUDIO_TYPE: "Audio"
+};
module.exports = MediaStreamType;
-},{}],88:[function(require,module,exports){
-var RTCBrowserType = {
- RTC_BROWSER_CHROME: "rtc_browser.chrome",
-
- RTC_BROWSER_FIREFOX: "rtc_browser.firefox"
-};
-
-module.exports = RTCBrowserType;
},{}],89:[function(require,module,exports){
-var RTCEvents = {
- LASTN_CHANGED: "rtc.lastn_changed",
- DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
- LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
- SIMULCAST_LAYER_CHANGED: "rtc.simulcast_layer_changed",
- SIMULCAST_LAYER_CHANGING: "rtc.simulcast_layer_changing",
- SIMULCAST_START: "rtc.simlcast_start",
- SIMULCAST_STOP: "rtc.simlcast_stop"
-};
-
-module.exports = RTCEvents;
+var RTCBrowserType = {
+ RTC_BROWSER_CHROME: "rtc_browser.chrome",
+
+ RTC_BROWSER_FIREFOX: "rtc_browser.firefox"
+};
+
+module.exports = RTCBrowserType;
},{}],90:[function(require,module,exports){
-var Resolutions = {
- "1080": {
- width: 1920,
- height: 1080,
- order: 7
- },
- "fullhd": {
- width: 1920,
- height: 1080,
- order: 7
- },
- "720": {
- width: 1280,
- height: 720,
- order: 6
- },
- "hd": {
- width: 1280,
- height: 720,
- order: 6
- },
- "960": {
- width: 960,
- height: 720,
- order: 5
- },
- "640": {
- width: 640,
- height: 480,
- order: 4
- },
- "vga": {
- width: 640,
- height: 480,
- order: 4
- },
- "360": {
- width: 640,
- height: 360,
- order: 3
- },
- "320": {
- width: 320,
- height: 240,
- order: 2
- },
- "180": {
- width: 320,
- height: 180,
- order: 1
- }
-};
-module.exports = Resolutions;
+var RTCEvents = {
+ LASTN_CHANGED: "rtc.lastn_changed",
+ DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
+ LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
+ SIMULCAST_LAYER_CHANGED: "rtc.simulcast_layer_changed",
+ SIMULCAST_LAYER_CHANGING: "rtc.simulcast_layer_changing",
+ SIMULCAST_START: "rtc.simlcast_start",
+ SIMULCAST_STOP: "rtc.simlcast_stop"
+};
+
+module.exports = RTCEvents;
},{}],91:[function(require,module,exports){
-var StreamEventTypes = {
- EVENT_TYPE_LOCAL_CREATED: "stream.local_created",
-
- EVENT_TYPE_LOCAL_CHANGED: "stream.local_changed",
-
- EVENT_TYPE_LOCAL_ENDED: "stream.local_ended",
-
- EVENT_TYPE_REMOTE_CREATED: "stream.remote_created",
-
- EVENT_TYPE_REMOTE_ENDED: "stream.remote_ended",
-
- EVENT_TYPE_REMOTE_CHANGED: "stream.changed"
-};
-
-module.exports = StreamEventTypes;
+var Resolutions = {
+ "1080": {
+ width: 1920,
+ height: 1080,
+ order: 7
+ },
+ "fullhd": {
+ width: 1920,
+ height: 1080,
+ order: 7
+ },
+ "720": {
+ width: 1280,
+ height: 720,
+ order: 6
+ },
+ "hd": {
+ width: 1280,
+ height: 720,
+ order: 6
+ },
+ "960": {
+ width: 960,
+ height: 720,
+ order: 5
+ },
+ "640": {
+ width: 640,
+ height: 480,
+ order: 4
+ },
+ "vga": {
+ width: 640,
+ height: 480,
+ order: 4
+ },
+ "360": {
+ width: 640,
+ height: 360,
+ order: 3
+ },
+ "320": {
+ width: 320,
+ height: 240,
+ order: 2
+ },
+ "180": {
+ width: 320,
+ height: 180,
+ order: 1
+ }
+};
+module.exports = Resolutions;
},{}],92:[function(require,module,exports){
-var UIEvents = {
- NICKNAME_CHANGED: "UI.nickname_changed",
- SELECTED_ENDPOINT: "UI.selected_endpoint",
- PINNED_ENDPOINT: "UI.pinned_endpoint"
-};
-module.exports = UIEvents;
+var StreamEventTypes = {
+ EVENT_TYPE_LOCAL_CREATED: "stream.local_created",
+
+ EVENT_TYPE_LOCAL_CHANGED: "stream.local_changed",
+
+ EVENT_TYPE_LOCAL_ENDED: "stream.local_ended",
+
+ EVENT_TYPE_REMOTE_CREATED: "stream.remote_created",
+
+ EVENT_TYPE_REMOTE_ENDED: "stream.remote_ended",
+
+ EVENT_TYPE_REMOTE_CHANGED: "stream.changed"
+};
+
+module.exports = StreamEventTypes;
},{}],93:[function(require,module,exports){
-var AuthenticationEvents = {
- /**
- * Event callback arguments:
- * function(authenticationEnabled, userIdentity)
- * authenticationEnabled - indicates whether authentication has been enabled
- * in this session
- * userIdentity - if user has been logged in then it contains user name. If
- * contains 'null' or 'undefined' then user is not logged in.
- */
- IDENTITY_UPDATED: "authentication.identity_updated"
-};
-module.exports = AuthenticationEvents;
-
+var UIEvents = {
+ NICKNAME_CHANGED: "UI.nickname_changed",
+ SELECTED_ENDPOINT: "UI.selected_endpoint",
+ PINNED_ENDPOINT: "UI.pinned_endpoint"
+};
+module.exports = UIEvents;
},{}],94:[function(require,module,exports){
-var CQEvents = {
- LOCALSTATS_UPDATED: "cq.localstats_updated",
- REMOTESTATS_UPDATED: "cq.remotestats_updated",
- STOP: "cq.stop"
-};
+var AuthenticationEvents = {
+ /**
+ * Event callback arguments:
+ * function(authenticationEnabled, userIdentity)
+ * authenticationEnabled - indicates whether authentication has been enabled
+ * in this session
+ * userIdentity - if user has been logged in then it contains user name. If
+ * contains 'null' or 'undefined' then user is not logged in.
+ */
+ IDENTITY_UPDATED: "authentication.identity_updated"
+};
+module.exports = AuthenticationEvents;
-module.exports = CQEvents;
},{}],95:[function(require,module,exports){
-var DesktopSharingEventTypes = {
- INIT: "ds.init",
-
- SWITCHING_DONE: "ds.switching_done",
-
- NEW_STREAM_CREATED: "ds.new_stream_created"
-};
-
-module.exports = DesktopSharingEventTypes;
+var CQEvents = {
+ LOCALSTATS_UPDATED: "cq.localstats_updated",
+ REMOTESTATS_UPDATED: "cq.remotestats_updated",
+ STOP: "cq.stop"
+};
+
+module.exports = CQEvents;
},{}],96:[function(require,module,exports){
-module.exports = {
- getLanguages : function () {
- var languages = [];
- for(var lang in this)
- {
- if(typeof this[lang] === "string")
- languages.push(this[lang]);
- }
- return languages;
- },
- EN: "en",
- BG: "bg",
- DE: "de",
- TR: "tr"
-}
+var DesktopSharingEventTypes = {
+ INIT: "ds.init",
+
+ SWITCHING_DONE: "ds.switching_done",
+
+ NEW_STREAM_CREATED: "ds.new_stream_created"
+};
+
+module.exports = DesktopSharingEventTypes;
},{}],97:[function(require,module,exports){
-var XMPPEvents = {
- CONFERENCE_CERATED: "xmpp.conferenceCreated.jingle",
- CALL_TERMINATED: "xmpp.callterminated.jingle",
- CALL_INCOMING: "xmpp.callincoming.jingle",
- DISPOSE_CONFERENCE: "xmpp.dispoce_confernce",
- GRACEFUL_SHUTDOWN: "xmpp.graceful_shutdown",
- KICKED: "xmpp.kicked",
- BRIDGE_DOWN: "xmpp.bridge_down",
- USER_ID_CHANGED: "xmpp.user_id_changed",
- CHANGED_STREAMS: "xmpp.changed_streams",
- MUC_JOINED: "xmpp.muc_joined",
- MUC_ENTER: "xmpp.muc_enter",
- MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
- MUC_LEFT: "xmpp.muc_left",
- MUC_DESTROYED: "xmpp.muc_destroyed",
- DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
- REMOTE_STATS: "xmpp.remote_stats",
- LOCALROLE_CHANGED: "xmpp.localrole_changed",
- PRESENCE_STATUS: "xmpp.presence_status",
- RESERVATION_ERROR: "xmpp.room_reservation_error",
- SUBJECT_CHANGED: "xmpp.subject_changed",
- MESSAGE_RECEIVED: "xmpp.message_received",
- SENDING_CHAT_MESSAGE: "xmpp.sending_chat_message",
- PASSWORD_REQUIRED: "xmpp.password_required",
- AUTHENTICATION_REQUIRED: "xmpp.authentication_required",
- CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
- ETHERPAD: "xmpp.etherpad"
-};
-module.exports = XMPPEvents;
+module.exports = {
+ getLanguages : function () {
+ var languages = [];
+ for(var lang in this)
+ {
+ if(typeof this[lang] === "string")
+ languages.push(this[lang]);
+ }
+ return languages;
+ },
+ EN: "en",
+ BG: "bg",
+ DE: "de",
+ TR: "tr"
+}
},{}],98:[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;
-}
-
-},{}]},{},[1])(1)
+var XMPPEvents = {
+ CONFERENCE_CERATED: "xmpp.conferenceCreated.jingle",
+ CALL_TERMINATED: "xmpp.callterminated.jingle",
+ CALL_INCOMING: "xmpp.callincoming.jingle",
+ DISPOSE_CONFERENCE: "xmpp.dispoce_confernce",
+ GRACEFUL_SHUTDOWN: "xmpp.graceful_shutdown",
+ KICKED: "xmpp.kicked",
+ BRIDGE_DOWN: "xmpp.bridge_down",
+ USER_ID_CHANGED: "xmpp.user_id_changed",
+ CHANGED_STREAMS: "xmpp.changed_streams",
+ MUC_JOINED: "xmpp.muc_joined",
+ MUC_ENTER: "xmpp.muc_enter",
+ MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
+ MUC_LEFT: "xmpp.muc_left",
+ MUC_DESTROYED: "xmpp.muc_destroyed",
+ DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
+ REMOTE_STATS: "xmpp.remote_stats",
+ LOCALROLE_CHANGED: "xmpp.localrole_changed",
+ PRESENCE_STATUS: "xmpp.presence_status",
+ RESERVATION_ERROR: "xmpp.room_reservation_error",
+ SUBJECT_CHANGED: "xmpp.subject_changed",
+ MESSAGE_RECEIVED: "xmpp.message_received",
+ SENDING_CHAT_MESSAGE: "xmpp.sending_chat_message",
+ PASSWORD_REQUIRED: "xmpp.password_required",
+ AUTHENTICATION_REQUIRED: "xmpp.authentication_required",
+ CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
+ ETHERPAD: "xmpp.etherpad"
+};
+module.exports = XMPPEvents;
+},{}]},{},[2])(2)
});
\ No newline at end of file
diff --git a/modules/desktopsharing/desktopsharing.js b/modules/desktopsharing/desktopsharing.js
index 1571e8cef..f9114b1d6 100644
--- a/modules/desktopsharing/desktopsharing.js
+++ b/modules/desktopsharing/desktopsharing.js
@@ -1,11 +1,13 @@
-/* global $, alert, changeLocalVideo, chrome, config, getConferenceHandler, getUserMediaWithConstraints */
+/* global $, alert, APP, changeLocalVideo, chrome, config, getConferenceHandler,
+ getUserMediaWithConstraints */
/**
* Indicates that desktop stream is currently in use(for toggle purpose).
* @type {boolean}
*/
var isUsingScreenStream = false;
/**
- * Indicates that switch stream operation is in progress and prevent from triggering new events.
+ * Indicates that switch stream operation is in progress and prevent from
+ * triggering new events.
* @type {boolean}
*/
var switchInProgress = false;
@@ -18,7 +20,21 @@ var switchInProgress = false;
var obtainDesktopStream = null;
/**
- * Flag used to cache desktop sharing enabled state. Do not use directly as it can be null .
+ * Indicates whether desktop sharing extension is installed.
+ * @type {boolean}
+ */
+var extInstalled = false;
+
+/**
+ * Indicates whether update of desktop sharing extension is required.
+ * @type {boolean}
+ */
+var extUpdateRequired = false;
+
+/**
+ * Flag used to cache desktop sharing enabled state. Do not use directly as
+ * it can be null .
+ *
* @type {null|boolean}
*/
var _desktopSharingEnabled = null;
@@ -27,7 +43,8 @@ var EventEmitter = require("events");
var eventEmitter = new EventEmitter();
-var DesktopSharingEventTypes = require("../../service/desktopsharing/DesktopSharingEventTypes");
+var DesktopSharingEventTypes
+ = require("../../service/desktopsharing/DesktopSharingEventTypes");
/**
* Method obtains desktop stream from WebRTC 'screen' source.
@@ -48,7 +65,8 @@ function obtainWebRTCScreen(streamCallback, failCallback) {
*/
function getWebStoreInstallUrl()
{
- return "https://chrome.google.com/webstore/detail/" + config.chromeExtensionId;
+ return "https://chrome.google.com/webstore/detail/" +
+ config.chromeExtensionId;
}
/**
@@ -98,11 +116,10 @@ function isUpdateRequired(minVersion, extVersion)
}
}
-
-function checkExtInstalled(isInstalledCallback) {
+function checkExtInstalled(callback) {
if (!chrome.runtime) {
// No API, so no extension for sure
- isInstalledCallback(false);
+ callback(false, false);
return;
}
chrome.runtime.sendMessage(
@@ -111,26 +128,24 @@ function checkExtInstalled(isInstalledCallback) {
function (response) {
if (!response || !response.version) {
// Communication failure - assume that no endpoint exists
- console.warn("Extension not installed?: " + chrome.runtime.lastError);
- isInstalledCallback(false);
- } else {
- // Check installed extension version
- var extVersion = response.version;
- console.log('Extension version is: ' + extVersion);
- var updateRequired = isUpdateRequired(config.minChromeExtVersion, extVersion);
- if (updateRequired) {
- alert(
- 'Jitsi Desktop Streamer requires update. ' +
- 'Changes will take effect after next Chrome restart.');
- }
- isInstalledCallback(!updateRequired);
+ console.warn(
+ "Extension not installed?: ", chrome.runtime.lastError);
+ callback(false, false);
+ return;
}
+ // Check installed extension version
+ var extVersion = response.version;
+ console.log('Extension version is: ' + extVersion);
+ var updateRequired
+ = isUpdateRequired(config.minChromeExtVersion, extVersion);
+ callback(!updateRequired, updateRequired);
}
);
}
function doGetStreamFromExtension(streamCallback, failCallback) {
- // Sends 'getStream' msg to the extension. Extension id must be defined in the config.
+ // Sends 'getStream' msg to the extension.
+ // Extension id must be defined in the config.
chrome.runtime.sendMessage(
config.chromeExtensionId,
{ getStream: true, sources: config.desktopSharingSources },
@@ -156,38 +171,44 @@ function doGetStreamFromExtension(streamCallback, failCallback) {
);
}
/**
- * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' stream for returned stream token.
+ * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop'
+ * stream for returned stream token.
*/
function obtainScreenFromExtension(streamCallback, failCallback) {
- checkExtInstalled(
- function (isInstalled) {
- if (isInstalled) {
- doGetStreamFromExtension(streamCallback, failCallback);
- } else {
- chrome.webstore.install(
- getWebStoreInstallUrl(),
- function (arg) {
- console.log("Extension installed successfully", arg);
- // We need to reload the page in order to get the access to chrome.runtime
- window.location.reload(false);
- },
- function (arg) {
- console.log("Failed to install the extension", arg);
- failCallback(arg);
- APP.UI.messageHandler.showError("dialog.error",
- "dialog.failtoinstall");
- }
- );
- }
+ if (extInstalled) {
+ doGetStreamFromExtension(streamCallback, failCallback);
+ } else {
+ if (extUpdateRequired) {
+ alert(
+ 'Jitsi Desktop Streamer requires update. ' +
+ 'Changes will take effect after next Chrome restart.');
}
- );
+
+ chrome.webstore.install(
+ getWebStoreInstallUrl(),
+ function (arg) {
+ console.log("Extension installed successfully", arg);
+ // We need to reload the page in order to get the access to
+ // chrome.runtime
+ window.location.reload(false);
+ },
+ function (arg) {
+ console.log("Failed to install the extension", arg);
+ failCallback(arg);
+ APP.UI.messageHandler.showError("dialog.error",
+ "dialog.failtoinstall");
+ }
+ );
+ }
}
/**
* Call this method to toggle desktop sharing feature.
- * @param method pass "ext" to use chrome extension for desktop capture(chrome extension required),
- * pass "webrtc" to use WebRTC "screen" desktop source('chrome://flags/#enable-usermedia-screen-capture'
- * must be enabled), pass any other string or nothing in order to disable this feature completely.
+ * @param method pass "ext" to use chrome extension for desktop capture(chrome
+ * extension required), pass "webrtc" to use WebRTC "screen" desktop
+ * source('chrome://flags/#enable-usermedia-screen-capture' must be
+ * enabled), pass any other string or nothing in order to disable this
+ * feature completely.
*/
function setDesktopSharing(method) {
// Check if we are running chrome
@@ -207,8 +228,9 @@ function setDesktopSharing(method) {
}
/**
- * Initializes with extension id set in config.js to support inline installs.
- * Host site must be selected as main website of published extension.
+ * Initializes with extension id set in
+ * config.js to support inline installs. Host site must be selected as main
+ * website of published extension.
*/
function initInlineInstalls()
{
@@ -240,19 +262,22 @@ module.exports = {
},
/**
- * @returns {boolean} true if desktop sharing feature is available and enabled.
+ * @returns {boolean} true if desktop sharing feature is available
+ * and enabled.
*/
isDesktopSharingEnabled: function () {
if (_desktopSharingEnabled === null) {
if (obtainDesktopStream === obtainScreenFromExtension) {
// Parse chrome version
var userAgent = navigator.userAgent.toLowerCase();
- // We can assume that user agent is chrome, because it's enforced when 'ext' streaming method is set
+ // We can assume that user agent is chrome, because it's
+ // enforced when 'ext' streaming method is set
var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
console.log("Chrome version" + userAgent, ver);
_desktopSharingEnabled = ver >= 34;
} else {
- _desktopSharingEnabled = obtainDesktopStream === obtainWebRTCScreen;
+ _desktopSharingEnabled =
+ obtainDesktopStream === obtainWebRTCScreen;
}
}
return _desktopSharingEnabled;
@@ -263,18 +288,28 @@ module.exports = {
// Initialize Chrome extension inline installs
if (config.chromeExtensionId) {
+
initInlineInstalls();
+
+ // Check if extension is installed
+ checkExtInstalled(function (installed, updateRequired) {
+ extInstalled = installed;
+ extUpdateRequired = updateRequired;
+ console.info(
+ "Chrome extension installed: " + extInstalled +
+ " updateRequired: " + extUpdateRequired);
+ });
}
eventEmitter.emit(DesktopSharingEventTypes.INIT);
},
- addListener: function(listener, type)
+ addListener: function (listener, type)
{
eventEmitter.on(type, listener);
},
- removeListener: function (listener,type) {
+ removeListener: function (listener, type) {
eventEmitter.removeListener(type, listener);
},
@@ -295,11 +330,12 @@ module.exports = {
function (stream) {
// We now use screen stream
isUsingScreenStream = true;
- // Hook 'ended' event to restore camera when screen stream stops
+ // Hook 'ended' event to restore camera
+ // when screen stream stops
stream.addEventListener('ended',
function (e) {
if (!switchInProgress && isUsingScreenStream) {
- toggleScreenSharing();
+ APP.desktopsharing.toggleScreenSharing();
}
}
);