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;
-
-},{}],22:[function(require,module,exports){
-/* global require, $ */
-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");
-var LargeVideo = require("../videolayout/LargeVideo");
-
-/**
- * Toggler for the chat, contact list, settings menu, etc..
- */
-var PanelToggler = (function(my) {
-
- var currentlyOpen = null;
- var buttons = {
- '#chatspace': '#chatBottomButton',
- '#contactlist': '#contactListButton',
- '#settingsmenu': '#toolbar_button_settings'
- };
-
- /**
- * 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 (LargeVideo.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');
- };
-
- VideoLayout.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');};
- VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
-
- toggle(ContactList,
- '#contactlist',
- null,
- function() {
- ContactList.setVisualNotification(false);
- },
- null);
- };
-
- /**
- * Opens / closes the settings menu
- */
- my.toggleSettingsMenu = function() {
- VideoLayout.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":31,"../util/UIUtil":35,"../videolayout/LargeVideo":37,"../videolayout/VideoLayout":41,"./../../settings/Settings":49,"./chat/Chat":23,"./contactlist/ContactList":27,"./settings/SettingsMenu":28}],23:[function(require,module,exports){
-/* global APP, $, 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 = $('#toolbar_button_chat');
- var bottomGlower = $('#chatBottomButton');
-
- if (unreadMessages) {
- unreadMsgElement.innerHTML = unreadMessages.toString();
- unreadMsgBottomElement.innerHTML = unreadMessages.toString();
-
- ToolbarToggler.dockToolbar(true);
-
- var chatButtonElement
- = document.getElementById('toolbar_button_chat');
- 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(stamp) {
- var now = (stamp? new Date(stamp): 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;
- }
- }
- });
-
- var usermsg = $('#usermsg');
- 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, myjid, stamp) {
- 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(stamp) +
- '
' + '
' + 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":160,"../../toolbars/ToolbarToggler":31,"../../util/NicknameHandler":34,"../../util/UIUtil":35,"../SidePanelToggler":22,"./Commands":24,"./Replacement":25,"./smileys.json":26}],24:[function(require,module,exports){
-/* global APP, require */
-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 a 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":35}],25:[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
-};
-
-},{"./smileys.json":26}],26:[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
- }
-}
-
-},{}],27:[function(require,module,exports){
-/* global $, APP, Strophe */
-var Avatar = require('../../avatar/Avatar');
-
-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) {
- numberOfContacts += delta;
- if (numberOfContacts === 1) {
- // when the user is alone we don't show the number of participants
- $("#numberOfParticipants").text('');
- ContactList.setVisualNotification(false);
- } else if (numberOfContacts > 1) {
- ContactList.setVisualNotification(!ContactList.isVisible());
- $("#numberOfParticipants").text(numberOfContacts);
- } else {
- console.error("Invalid number of participants: " + numberOfContacts);
- }
-}
-
-/**
- * Creates the avatar element.
- *
- * @return {object} the newly created avatar element
- */
-function createAvatar(jid) {
- var avatar = document.createElement('img');
- avatar.className = "icon-avatar avatar";
- avatar.src = Avatar.getContactListUrl(jid);
-
- 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
- */
- ensureAddContact: function (peerJid) {
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contact = $('#contacts>li[id="' + resourceJid + '"]');
-
- if (!contact || contact.length <= 0)
- ContactList.addContact(peerJid);
- },
-
- /**
- * Adds a contact for the given peer jid.
- *
- * @param peerJid the jid of the contact to add
- */
- addContact: function (peerJid) {
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- var contactlist = $('#contacts');
-
- 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(peerJid));
- newContact.appendChild(createDisplayNameParagraph("participant"));
-
- if (resourceJid === APP.xmpp.myResource()) {
- contactlist.prepend(newContact);
- }
- else {
- contactlist.append(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 = $('#contacts>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 = $('#contacts>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 = $('#contacts #' + resourceJid + '>p');
-
- if (contactName && displayName && displayName.length > 0)
- contactName.html(displayName);
- },
-
- userAvatarChanged: function (resourceJid, contactListUrl) {
- // set the avatar in the contact list
- var contact = $('#' + resourceJid + '>img');
- if (contact && contact.length > 0) {
- contact.get(0).src = contactListUrl;
- }
-
- }
-};
-
-module.exports = ContactList;
-},{"../../avatar/Avatar":18}],28:[function(require,module,exports){
-/* global APP, $ */
-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 () {
- var startMutedSelector = $("#startMutedOptions");
- startMutedSelector.before(generateLanguagesSelectBox());
- APP.translation.translateElement($("#languages_selectbox"));
- $('#settingsmenu>input').keyup(function(event){
- if(event.keyCode === 13) {//enter
- SettingsMenu.update();
- }
- });
-
- if (APP.xmpp.isModerator()) {
- startMutedSelector.css("display", "block");
- }
- else {
- startMutedSelector.css("display", "none");
- }
-
- $("#updateSettings").click(function () {
- SettingsMenu.update();
- });
- },
-
- onRoleChanged: function () {
- if(APP.xmpp.isModerator()) {
- $("#startMutedOptions").css("display", "block");
- }
- else {
- $("#startMutedOptions").css("display", "none");
- }
- },
-
- setStartMuted: function (audio, video) {
- $("#startAudioMuted").attr("checked", audio);
- $("#startVideoMuted").attr("checked", video);
- },
-
- 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);
-
- var startAudioMuted = ($("#startAudioMuted").is(":checked"));
- var startVideoMuted = ($("#startVideoMuted").is(":checked"));
- APP.xmpp.addToPresence("startMuted",
- [startAudioMuted, startVideoMuted]);
-
- 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);
- }
- },
- changeAvatar: function (thumbUrl) {
- $('#avatar').get(0).src = thumbUrl;
- }
-};
-
-
-module.exports = SettingsMenu;
-},{"../../../../service/translation/languages":165,"../../avatar/Avatar":18,"../../util/UIUtil":35,"./../../../settings/Settings":49}],29:[function(require,module,exports){
-/* global $ */
-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":22}],30:[function(require,module,exports){
-/* global APP, $, buttonClick, config, lockRoom, interfaceConfig, 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 recordingToaster = 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("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
- return Toolbar.toggleFullScreen();
- },
- "toolbar_button_sip": function () {
- return callSipButtonClicked();
- },
- "toolbar_button_dialpad": function () {
- return dialpadButtonClicked();
- },
- "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.generateTranslationHTML(
- "dialog.sessTerminated");
- var msg = APP.translation.generateTranslationHTML(
- "dialog.hungUp");
- var button = APP.translation.generateTranslationHTML(
- "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(predefinedToken) {
- APP.xmpp.toggleRecording(function (callback) {
- if (predefinedToken) {
- callback(UIUtil.escapeHtml(predefinedToken));
- return;
- }
-
- var msg = APP.translation.generateTranslationHTML(
- "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);
-}
-
-/**
- * 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 dialpadButtonClicked() {
- //TODO show the dialpad box
-}
-
-function callSipButtonClicked() {
- var defaultNumber
- = config.defaultSipNumber ? config.defaultSipNumber : '';
-
- var sipMsg = APP.translation.generateTranslationHTML(
- "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, 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.isMUCJoined()) {
- 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();
- $('#inviteLinkRef').parent()
- .find('button[value=true]').prop('disabled', false);
- }
- };
-
- /**
- * Disables and enables some of the buttons.
- */
- my.setupButtonsFromConfig = function () {
- if (config.disablePrezi) {
- $("#toolbar_button_prezi").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.generateTranslationHTML(
- "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 (event) {
- if (roomUrl) {
- document.getElementById('inviteLinkRef').select();
- } else {
- if (event && event.target)
- $(event.target)
- .find('button[value=true]').prop('disabled', true);
- }
- }
- );
- };
-
- /**
- * Opens the settings dialog.
- * FIXME: not used ?
- */
- my.openSettingsDialog = function () {
- var settings1 = APP.translation.generateTranslationHTML(
- "dialog.settings1");
- var settings2 = APP.translation.generateTranslationHTML(
- "dialog.settings2");
- var settings3 = APP.translation.generateTranslationHTML(
- "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 ($("#toolbar_button_security").hasClass("icon-security-locked"))
- UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
- };
- /**
- * Updates the lock button state to locked.
- */
- my.lockLockButton = function () {
- if ($("#toolbar_button_security").hasClass("icon-security"))
- UIUtil.buttonClick("#toolbar_button_security", "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) {
- $('#toolbar_button_record').css({display: "inline-block"});
- }
- else {
- $('#toolbar_button_record').css({display: "none"});
- }
- };
-
- // Sets the state of the recording button
- my.setRecordingButtonState = function (recordingState) {
- var selector = $('#toolbar_button_record');
-
- if (recordingState === 'on') {
- selector.removeClass("icon-recEnable");
- selector.addClass("icon-recEnable active");
-
- $("#largeVideo").toggleClass("videoMessageFilter", true);
- var recordOnKey = "recording.on";
- $('#videoConnectionMessage').attr("data-i18n", recordOnKey);
- $('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
-
- setTimeout(function(){
- $("#largeVideo").toggleClass("videoMessageFilter", false);
- $('#videoConnectionMessage').css({display: "none"});
- }, 1500);
-
- recordingToaster = messageHandler.notify(null, "recording.toaster", null,
- null, null, {timeOut: 0, closeButton: null, tapToDismiss: false});
- } else if (recordingState === 'off') {
- selector.removeClass("icon-recEnable active");
- selector.addClass("icon-recEnable");
-
- $("#largeVideo").toggleClass("videoMessageFilter", false);
- $('#videoConnectionMessage').css({display: "none"});
-
- if (recordingToaster)
- messageHandler.remove(recordingToaster);
-
- } else if (recordingState === 'pending') {
- selector.removeClass("icon-recEnable active");
- selector.addClass("icon-recEnable");
-
- $("#largeVideo").toggleClass("videoMessageFilter", true);
- var recordPendingKey = "recording.pending";
- $('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
- $('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
- $('#videoConnectionMessage').css({display: "block"});
- }
- };
-
- // checks whether recording is enabled and whether we have params to start automatically recording
- my.checkAutoRecord = function () {
- if (config.enableRecording && config.autoRecord) {
- toggleRecording(config.autoRecordToken);
- }
- }
-
- // Shows or hides SIP calls button
- my.showSipCallButton = function (show) {
- if (APP.xmpp.isSipGatewayEnabled() && show) {
- $('#toolbar_button_sip').css({display: "inline-block"});
- } else {
- $('#toolbar_button_sip').css({display: "none"});
- }
- };
-
- // Shows or hides the dialpad button
- my.showDialPadButton = function (show) {
- if (show) {
- $('#toolbar_button_dialpad').css({display: "inline-block"});
- } else {
- $('#toolbar_button_dialpad').css({display: "none"});
- }
- };
-
- /**
- * Displays user authenticated identity name(login).
- * @param authIdentity identity name to be displayed.
- */
- my.setAuthenticatedIdentity = function (authIdentity) {
- if (authIdentity) {
- var selector = $('#toolbar_auth_identity');
- selector.css({display: "list-item"});
- selector.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 = $("#toolbar_button_desktopsharing");
- if (active) {
- button.addClass("glow");
- } else {
- button.removeClass("glow");
- }
- };
-
- return my;
-}(Toolbar || {}));
-
-module.exports = Toolbar;
-},{"../../../service/authentication/AuthenticationEvents":161,"../authentication/Authentication":16,"../etherpad/Etherpad":19,"../prezi/Prezi":20,"../side_pannels/SidePanelToggler":22,"../util/MessageHandler":33,"../util/UIUtil":35,"./BottomToolbar":29}],31:[function(require,module,exports){
-/* global APP, config, $, interfaceConfig, Moderator,
- DesktopStreaming.showDesktopSharingButton */
-
-var toolbarTimeoutObject,
- toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
-
-function showDesktopSharingButton() {
- if (APP.desktopsharing.isDesktopSharingEnabled()) {
- $('#toolbar_button_desktopsharing').css({display: "inline-block"});
- } else {
- $('#toolbar_button_desktopsharing').css({display: "none"});
- }
-}
-
-/**
- * Hides the toolbar.
- */
-function hideToolbar() {
- if(config.alwaysVisibleToolbar)
- return;
-
- 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 () {
- if (interfaceConfig.filmStripOnly)
- return;
- 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 (interfaceConfig.filmStripOnly)
- return;
-
- 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;
-},{}],32:[function(require,module,exports){
-/* global $ */
-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 () {
- if(!JitsiPopover.enabled)
- return;
- 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();
- };
-
- JitsiPopover.enabled = true;
-
- return JitsiPopover;
-})();
-
-module.exports = JitsiPopover;
-},{}],33:[function(require,module,exports){
-/* global $, APP, jQuery, toastr */
-
-/**
- * Flag for enable/disable of the notifications.
- * @type {boolean}
- */
-var notificationsEnabled = true;
-
-var messageHandler = (function(my) {
-
- /**
- * Shows a message to the user.
- *
- * @param titleKey the title of the message
- * @param messageKey the text of the message
- */
- my.openMessageDialog = function(titleKey, messageKey) {
- var title = null;
- if(titleKey) {
- title = APP.translation.generateTranslationHTML(titleKey);
- }
- var message = APP.translation.generateTranslationHTML(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.generateTranslationHTML(leftButtonKey);
- buttons.push({ title: leftButton, value: true});
-
- var cancelButton
- = APP.translation.generateTranslationHTML("dialog.Cancel");
- buttons.push({title: cancelButton, value: false});
-
- var message = msgString, title = titleString;
- if (titleKey) {
- title = APP.translation.generateTranslationHTML(titleKey);
- }
- if (msgKey) {
- message = APP.translation.generateTranslationHTML(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 {object} 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 titleKey the title of the message
- * @param msgKey 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 titleKey the title of the message.
- * @param msgKey the text of the message.
- */
- my.showError = function(titleKey, msgKey) {
-
- if (!titleKey) {
- titleKey = "dialog.oops";
- }
- if (!msgKey) {
- msgKey = "dialog.defaultError";
- }
- messageHandler.openMessageDialog(titleKey, msgKey);
- };
-
- /**
- * Displayes notification.
- * @param displayName display name of the participant that is associated with the notification.
- * @param displayNameKey the key from the language file for the display name.
- * @param cls css class for the notification
- * @param messageKey the key from the language file for the text of the message.
- * @param messageArguments object with the arguments for the message.
- * @param options object with language options.
- */
- my.notify = function(displayName, displayNameKey,
- cls, messageKey, messageArguments, options) {
- if(!notificationsEnabled)
- return;
- var displayNameSpan = '" + displayName;
- } else {
- displayNameSpan += "data-i18n='" + displayNameKey +
- "'>" + APP.translation.translateString(displayNameKey);
- }
- displayNameSpan += " ";
- return toastr.info(
- displayNameSpan + ' ' +
- '" +
- APP.translation.translateString(messageKey,
- messageArguments) +
- ' ', null, options);
- };
-
- /**
- * Removes the toaster.
- * @param toasterElement
- */
- my.remove = function(toasterElement) {
- toasterElement.remove();
- };
-
- /**
- * Disables notifications.
- */
- my.disableNotifications = function () {
- notificationsEnabled = false;
- };
-
- /**
- * Enables notifications.
- */
- my.enableNotifications = function () {
- notificationsEnabled = true;
- };
-
- return my;
-}(messageHandler || {}));
-
-module.exports = messageHandler;
-
-
-
-},{}],34:[function(require,module,exports){
-var UIEvents = require("../../../service/UI/UIEvents");
-
-var nickname = null;
-var eventEmitter = null;
-
-var NicknameHandler = {
- 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 = NicknameHandler;
-},{"../../../service/UI/UIEvents":160}],35:[function(require,module,exports){
-/* global $ */
-/**
- * Created by hristo on 12/22/14.
- */
-module.exports = {
- /**
- * Returns the available video width.
- */
- getAvailableVideoWidth: function (isVisible) {
- var PanelToggler = require("../side_pannels/SidePanelToggler");
- if(typeof isVisible === "undefined" || isVisible === null)
- isVisible = PanelToggler.isVisible();
- var rightPanelWidth
- = 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] * 0.3 + pixels[i+1] * 0.59 + pixels[i+2] * 0.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");
- },
-
- /**
- * Inserts given child element as the first one into the container.
- * @param container the container to which new child element will be added
- * @param newChild the new element that will be inserted into the container
- */
- prependChild: function (container, newChild) {
- var firstChild = container.childNodes[0];
- if (firstChild) {
- container.insertBefore(newChild, firstChild);
- } else {
- container.appendChild(newChild);
- }
- }
+ var localAudio = APP.RTC.localAudio;
+ if (localAudio && localAudio.getTracks().length > 0)
+ {
+ var peerconnection
+ = APP.xmpp.getConnection().jingle.activecall.peerconnection;
+ if (peerconnection) {
+ DTMFSender =
+ peerconnection.peerconnection
+ .createDTMFSender(localAudio.getTracks()[0]);
+ console.log("Initialized DTMFSender");
+ }
+ else {
+ console.log("Failed to initialize DTMFSender: no PeerConnection.");
+ }
+ }
+ else {
+ console.log("Failed to initialize DTMFSender: no audio track.");
+ }
};
-},{"../side_pannels/SidePanelToggler":22}],36:[function(require,module,exports){
-/* global APP, $ */
-var JitsiPopover = require("../util/JitsiPopover");
-
-/**
- * Constructs new connection indicator.
- * @param videoContainer the video container associated with the indicator.
- * @constructor
- */
-function ConnectionIndicator(videoContainer, jid) {
- 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();
-}
-
-/**
- * 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);
- for(var ssrc in this.resolution) {
- resolutionValue = this.resolution[ssrc];
- }
- }
-
- 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.videoSpanId == "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.container.appendChild(
- this.connectionIndicatorContainer);
- this.popover = new JitsiPopover(
- $("#" + this.videoContainer.videoSpanId + " > .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() {
- if (this.connectionIndicatorContainer.parentNode) {
- this.connectionIndicatorContainer.parentNode.removeChild(
- this.connectionIndicatorContainer);
- }
- 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.videoContainer.updateIconPositions();
- }
- }
- 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();
-};
-
+
+var DTMF = {
+ sendTones: function (tones, duration, pause) {
+ if (!DTMFSender)
+ initDtmfSender();
+
+ if (DTMFSender){
+ DTMFSender.insertDTMF(tones,
+ (duration || 200),
+ (pause || 200));
+ }
+ }
+};
+
+module.exports = DTMF;
+
+
+},{}],4:[function(require,module,exports){
+/* global config, APP, Strophe */
+
+// cache datachannels to avoid garbage collection
+// https://code.google.com/p/chromium/issues/detail?id=405545
+var RTCEvents = require("../../service/RTC/RTCEvents");
+
+var _dataChannels = [];
+var eventEmitter = null;
+
+
+var DataChannels = {
+ /**
+ * Callback triggered by PeerConnection when new data channel is opened
+ * on the bridge.
+ * @param event the event info object.
+ */
+ onDataChannel: function (event) {
+ var dataChannel = event.channel;
+
+ dataChannel.onopen = function () {
+ console.info("Data channel opened by the Videobridge!", dataChannel);
+
+ // Code sample for sending string and/or binary data
+ // Sends String message to the bridge
+ //dataChannel.send("Hello bridge!");
+ // Sends 12 bytes binary message to the bridge
+ //dataChannel.send(new ArrayBuffer(12));
+
+ eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
+ };
+
+ dataChannel.onerror = function (error) {
+ console.error("Data Channel Error:", error, dataChannel);
+ };
+
+ dataChannel.onmessage = function (event) {
+ var data = event.data;
+ // JSON
+ var obj;
+
+ try {
+ obj = JSON.parse(data);
+ }
+ catch (e) {
+ console.error(
+ "Failed to parse data channel message as JSON: ",
+ data,
+ dataChannel);
+ }
+ if (('undefined' !== typeof(obj)) && (null !== obj)) {
+ var colibriClass = obj.colibriClass;
+
+ if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
+ // Endpoint ID from the Videobridge.
+ var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
+
+ console.info(
+ "Data channel new dominant speaker event: ",
+ dominantSpeakerEndpoint);
+ eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
+ }
+ else if ("InLastNChangeEvent" === colibriClass) {
+ var oldValue = obj.oldValue;
+ var newValue = obj.newValue;
+ // Make sure that oldValue and newValue are of type boolean.
+ var type;
+
+ if ((type = typeof oldValue) !== 'boolean') {
+ if (type === 'string') {
+ oldValue = (oldValue == "true");
+ } else {
+ oldValue = new Boolean(oldValue).valueOf();
+ }
+ }
+ if ((type = typeof newValue) !== 'boolean') {
+ if (type === 'string') {
+ newValue = (newValue == "true");
+ } else {
+ newValue = new Boolean(newValue).valueOf();
+ }
+ }
+
+ eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
+ }
+ else if ("LastNEndpointsChangeEvent" === colibriClass) {
+ // The new/latest list of last-n endpoint IDs.
+ var lastNEndpoints = obj.lastNEndpoints;
+ // The list of endpoint IDs which are entering the list of
+ // last-n at this time i.e. were not in the old list of last-n
+ // endpoint IDs.
+ var endpointsEnteringLastN = obj.endpointsEnteringLastN;
+
+ console.log(
+ "Data channel new last-n event: ",
+ lastNEndpoints, endpointsEnteringLastN, obj);
+ eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
+ lastNEndpoints, endpointsEnteringLastN, obj);
+ }
+ else {
+ console.debug("Data channel JSON-formatted message: ", obj);
+ }
+ }
+ };
+
+ dataChannel.onclose = function () {
+ console.info("The Data Channel closed", dataChannel);
+ var idx = _dataChannels.indexOf(dataChannel);
+ if (idx > -1)
+ _dataChannels = _dataChannels.splice(idx, 1);
+ };
+ _dataChannels.push(dataChannel);
+ },
+
+ /**
+ * Binds "ondatachannel" event listener to given PeerConnection instance.
+ * @param peerConnection WebRTC peer connection instance.
+ */
+ init: function (peerConnection, emitter) {
+ if(!config.openSctp)
+ return;
+
+ peerConnection.ondatachannel = this.onDataChannel;
+ eventEmitter = emitter;
+
+ // Sample code for opening new data channel from Jitsi Meet to the bridge.
+ // Although it's not a requirement to open separate channels from both bridge
+ // and peer as single channel can be used for sending and receiving data.
+ // So either channel opened by the bridge or the one opened here is enough
+ // for communication with the bridge.
+ /*var dataChannelOptions =
+ {
+ reliable: true
+ };
+ var dataChannel
+ = peerConnection.createDataChannel("myChannel", dataChannelOptions);
+
+ // Can be used only when is in open state
+ dataChannel.onopen = function ()
+ {
+ dataChannel.send("My channel !!!");
+ };
+ dataChannel.onmessage = function (event)
+ {
+ var msgData = event.data;
+ console.info("Got My Data Channel Message:", msgData, dataChannel);
+ };*/
+ },
+ handleSelectedEndpointEvent: onSelectedEndpointChanged,
+ handlePinnedEndpointEvent: onPinnedEndpointChanged
+};
+
+function onSelectedEndpointChanged(userResource) {
+ console.log('selected endpoint changed: ', userResource);
+ if (_dataChannels && _dataChannels.length != 0) {
+ _dataChannels.some(function (dataChannel) {
+ if (dataChannel.readyState == 'open') {
+ console.log('sending selected endpoint changed ' +
+ 'notification to the bridge: ', userResource);
+ dataChannel.send(JSON.stringify({
+ 'colibriClass': 'SelectedEndpointChangedEvent',
+ 'selectedEndpoint':
+ (!userResource || userResource === null)?
+ null : userResource
+ }));
+
+ return true;
+ }
+ });
+ }
+}
+
+function onPinnedEndpointChanged(userResource) {
+ console.log('pinned endpoint changed: ', userResource);
+ if (_dataChannels && _dataChannels.length != 0) {
+ _dataChannels.some(function (dataChannel) {
+ if (dataChannel.readyState == 'open') {
+ dataChannel.send(JSON.stringify({
+ 'colibriClass': 'PinnedEndpointChangedEvent',
+ 'pinnedEndpoint':
+ (!userResource || userResource == null)?
+ null : userResource
+ }));
+
+ return true;
+ }
+ });
+ }
+}
+
+module.exports = DataChannels;
+
+
+},{"../../service/RTC/RTCEvents":160}],5:[function(require,module,exports){
+/* global APP */
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
+var RTCEvents = require("../../service/RTC/RTCEvents");
+var RTCBrowserType = require("./RTCBrowserType");
+
+/**
+ * This implements 'onended' callback normally fired by WebRTC after the stream
+ * is stopped. There is no such behaviour yet in FF, so we have to add it.
+ * @param stream original WebRTC stream object to which 'onended' handling
+ * will be added.
+ */
+function implementOnEndedHandling(stream) {
+ var originalStop = stream.stop;
+ stream.stop = function () {
+ originalStop.apply(stream);
+ if (!stream.ended) {
+ stream.ended = true;
+ stream.onended();
+ }
+ };
+}
+
+function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) {
+ this.stream = stream;
+ this.eventEmitter = eventEmitter;
+ this.type = type;
+ this.videoType = videoType;
+ this.isGUMStream = true;
+ if(isGUMStream === false)
+ this.isGUMStream = isGUMStream;
+ var self = this;
+ if(type == "audio") {
+ this.getTracks = function () {
+ return self.stream.getAudioTracks();
+ };
+ } else {
+ this.getTracks = function () {
+ return self.stream.getVideoTracks();
+ };
+ }
+
+ this.stream.onended = function () {
+ self.streamEnded();
+ };
+ if (RTCBrowserType.isFirefox()) {
+ implementOnEndedHandling(this.stream);
+ }
+}
+
+LocalStream.prototype.streamEnded = function () {
+ this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
+};
+
+LocalStream.prototype.getOriginalStream = function()
+{
+ return this.stream;
+};
+
+LocalStream.prototype.isAudioStream = function () {
+ return this.type === "audio";
+};
+
+LocalStream.prototype.setMute = function (mute)
+{
+ var isAudio = this.isAudioStream();
+ var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE;
+
+ if ((window.location.protocol != "https:" && this.isGUMStream) ||
+ (isAudio && this.isGUMStream) || this.videoType === "screen" ||
+ // FIXME FF does not support 'removeStream' method used to mute
+ RTCBrowserType.isFirefox()) {
+
+ var tracks = this.getTracks();
+ for (var idx = 0; idx < tracks.length; idx++) {
+ tracks[idx].enabled = !mute;
+ }
+ this.eventEmitter.emit(eventType, mute);
+ } else {
+ if (mute) {
+ APP.xmpp.removeStream(this.stream);
+ this.stream.stop();
+ this.eventEmitter.emit(eventType, true);
+ } else {
+ var self = this;
+ APP.RTC.rtcUtils.obtainAudioAndVideoPermissions(
+ (this.isAudioStream() ? ["audio"] : ["video"]),
+ function (stream) {
+ if (isAudio) {
+ APP.RTC.changeLocalAudio(stream,
+ function () {
+ self.eventEmitter.emit(eventType, false);
+ });
+ } else {
+ APP.RTC.changeLocalVideo(stream, false,
+ function () {
+ self.eventEmitter.emit(eventType, false);
+ });
+ }
+ });
+ }
+ }
+};
+
+LocalStream.prototype.isMuted = function () {
+ var tracks = [];
+ if (this.isAudioStream()) {
+ tracks = this.stream.getAudioTracks();
+ } else {
+ if (this.stream.ended)
+ return true;
+ tracks = this.stream.getVideoTracks();
+ }
+ for (var idx = 0; idx < tracks.length; idx++) {
+ if(tracks[idx].enabled)
+ return false;
+ }
+ return true;
+};
+
+LocalStream.prototype.getId = function () {
+ return this.stream.getTracks()[0].id;
+};
+
+module.exports = LocalStream;
+
+},{"../../service/RTC/RTCEvents":160,"../../service/RTC/StreamEventTypes.js":162,"./RTCBrowserType":8}],6:[function(require,module,exports){
+var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
+
+/**
+ * Creates a MediaStream object for the given data, session id and ssrc.
+ * It is a wrapper class for the MediaStream.
+ *
+ * @param data the data object from which we obtain the stream,
+ * the peerjid, etc.
+ * @param sid the session id
+ * @param ssrc the ssrc corresponding to this MediaStream
+ *
+ * @constructor
+ */
+function MediaStream(data, sid, ssrc, browser, eventEmitter) {
+
+ // XXX(gp) to minimize headaches in the future, we should build our
+ // abstractions around tracks and not streams. ORTC is track based API.
+ // Mozilla expects m-lines to represent media tracks.
+ //
+ // Practically, what I'm saying is that we should have a MediaTrack class
+ // and not a MediaStream class.
+ //
+ // Also, we should be able to associate multiple SSRCs with a MediaTrack as
+ // a track might have an associated RTX and FEC sources.
+
+ this.sid = sid;
+ this.stream = data.stream;
+ this.peerjid = data.peerjid;
+ this.videoType = data.videoType;
+ this.ssrc = ssrc;
+ this.type = (this.stream.getVideoTracks().length > 0)?
+ MediaStreamType.VIDEO_TYPE : MediaStreamType.AUDIO_TYPE;
+ this.muted = false;
+ this.eventEmitter = eventEmitter;
+}
+
+
+MediaStream.prototype.getOriginalStream = function() {
+ return this.stream;
+};
+
+MediaStream.prototype.setMute = function (value) {
+ this.stream.muted = value;
+ this.muted = value;
+};
+
+module.exports = MediaStream;
+
+},{"../../service/RTC/MediaStreamTypes":159}],7:[function(require,module,exports){
+/* global APP */
+var EventEmitter = require("events");
+var RTCBrowserType = require("./RTCBrowserType");
+var RTCUtils = require("./RTCUtils.js");
+var LocalStream = require("./LocalStream.js");
+var DataChannels = require("./DataChannels");
+var MediaStream = require("./MediaStream.js");
+var DesktopSharingEventTypes
+ = require("../../service/desktopsharing/DesktopSharingEventTypes");
+var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
+var RTCEvents = require("../../service/RTC/RTCEvents.js");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var UIEvents = require("../../service/UI/UIEvents");
+
+var eventEmitter = new EventEmitter();
+
+
+function getMediaStreamUsage()
+{
+ var result = {
+ audio: true,
+ video: true
+ };
+
+ /** There are some issues with the desktop sharing
+ * when this property is enabled.
+ * WARNING: We must change the implementation to start video/audio if we
+ * receive from the focus that the peer is not muted.
+
+ var isSecureConnection = window.location.protocol == "https:";
+
+ if(config.disableEarlyMediaPermissionRequests || !isSecureConnection)
+ {
+ result = {
+ audio: false,
+ video: false
+ };
+
+ }
+ **/
+
+ return result;
+}
+
+var RTC = {
+ rtcUtils: null,
+ devices: {
+ audio: true,
+ video: true
+ },
+ localStreams: [],
+ remoteStreams: {},
+ localAudio: null,
+ localVideo: null,
+ addStreamListener: function (listener, eventType) {
+ eventEmitter.on(eventType, listener);
+ },
+ addListener: function (type, listener) {
+ eventEmitter.on(type, listener);
+ },
+ removeStreamListener: function (listener, eventType) {
+ if(!(eventType instanceof StreamEventTypes))
+ throw "Illegal argument";
+
+ eventEmitter.removeListener(eventType, listener);
+ },
+ createLocalStream: function (stream, type, change, videoType, isMuted, isGUMStream) {
+
+ var localStream = new LocalStream(stream, type, eventEmitter, videoType, isGUMStream);
+ //in firefox we have only one stream object
+ if(this.localStreams.length == 0 ||
+ this.localStreams[0].getOriginalStream() != stream)
+ this.localStreams.push(localStream);
+ if(isMuted === true)
+ localStream.setMute(true);
+
+ if(type == "audio") {
+ this.localAudio = localStream;
+ } else {
+ this.localVideo = localStream;
+ }
+ var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED;
+ if(change)
+ eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED;
+
+ eventEmitter.emit(eventType, localStream, isMuted);
+ return localStream;
+ },
+ removeLocalStream: function (stream) {
+ for(var i = 0; i < this.localStreams.length; i++) {
+ if(this.localStreams[i].getOriginalStream() === stream) {
+ delete this.localStreams[i];
+ return;
+ }
+ }
+ },
+ createRemoteStream: function (data, sid, thessrc) {
+ var remoteStream = new MediaStream(data, sid, thessrc,
+ RTCBrowserType.getBrowserType(), eventEmitter);
+ var jid = data.peerjid || APP.xmpp.myJid();
+ if(!this.remoteStreams[jid]) {
+ this.remoteStreams[jid] = {};
+ }
+ this.remoteStreams[jid][remoteStream.type]= remoteStream;
+ eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, remoteStream);
+ return remoteStream;
+ },
+ getPCConstraints: function () {
+ return this.rtcUtils.pc_constraints;
+ },
+ getUserMediaWithConstraints:function(um, success_callback,
+ failure_callback, resolution,
+ bandwidth, fps, desktopStream)
+ {
+ return this.rtcUtils.getUserMediaWithConstraints(um, success_callback,
+ failure_callback, resolution, bandwidth, fps, desktopStream);
+ },
+ attachMediaStream: function (elSelector, stream) {
+ this.rtcUtils.attachMediaStream(elSelector, stream);
+ },
+ getStreamID: function (stream) {
+ return this.rtcUtils.getStreamID(stream);
+ },
+ getVideoSrc: function (element) {
+ return this.rtcUtils.getVideoSrc(element);
+ },
+ setVideoSrc: function (element, src) {
+ this.rtcUtils.setVideoSrc(element, src);
+ },
+ getVideoElementName: function () {
+ return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video';
+ },
+ dispose: function() {
+ if (this.rtcUtils) {
+ this.rtcUtils = null;
+ }
+ },
+ stop: function () {
+ this.dispose();
+ },
+ start: function () {
+ var self = this;
+ APP.desktopsharing.addListener(
+ function (stream, isUsingScreenStream, callback) {
+ self.changeLocalVideo(stream, isUsingScreenStream, callback);
+ }, DesktopSharingEventTypes.NEW_STREAM_CREATED);
+ APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function(event) {
+ DataChannels.init(event.peerconnection, eventEmitter);
+ });
+ APP.UI.addListener(UIEvents.SELECTED_ENDPOINT,
+ DataChannels.handleSelectedEndpointEvent);
+ APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
+ DataChannels.handlePinnedEndpointEvent);
+
+ // In case of IE we continue from 'onReady' callback
+ // passed to RTCUtils constructor. It will be invoked by Temasys plugin
+ // once it is initialized.
+ var onReady = function () {
+ eventEmitter.emit(RTCEvents.RTC_READY, true);
+ self.rtcUtils.obtainAudioAndVideoPermissions(
+ null, null, getMediaStreamUsage());
+ };
+
+ this.rtcUtils = new RTCUtils(this, onReady);
+
+ // Call onReady() if Temasys plugin is not used
+ if (!RTCBrowserType.isTemasysPluginUsed()) {
+ onReady();
+ }
+ },
+ muteRemoteVideoStream: function (jid, value) {
+ var stream;
+
+ if(this.remoteStreams[jid] &&
+ this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
+ stream = this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
+ }
+
+ if(!stream)
+ return true;
+
+ if (value != stream.muted) {
+ stream.setMute(value);
+ return true;
+ }
+ return false;
+ },
+ switchVideoStreams: function (new_stream) {
+ this.localVideo.stream = new_stream;
+
+ this.localStreams = [];
+
+ //in firefox we have only one stream object
+ if (this.localAudio.getOriginalStream() != new_stream)
+ this.localStreams.push(this.localAudio);
+ this.localStreams.push(this.localVideo);
+ },
+ changeLocalVideo: function (stream, isUsingScreenStream, callback) {
+ var oldStream = this.localVideo.getOriginalStream();
+ var type = (isUsingScreenStream ? "screen" : "camera");
+ var localCallback = callback;
+ if(this.localVideo.isMuted() && this.localVideo.videoType !== type) {
+ localCallback = function() {
+ APP.xmpp.setVideoMute(false, function(mute) {
+ eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
+ });
+
+ callback();
+ };
+ }
+ // FIXME: Workaround for FF/IE/Safari
+ if (stream && stream.videoStream) {
+ stream = stream.videoStream;
+ }
+ var videoStream = this.rtcUtils.createStream(stream, true);
+ this.localVideo = this.createLocalStream(videoStream, "video", true, type);
+ // Stop the stream to trigger onended event for old stream
+ oldStream.stop();
+
+ this.switchVideoStreams(videoStream, oldStream);
+
+ APP.xmpp.switchStreams(videoStream, oldStream,localCallback);
+ },
+ changeLocalAudio: function (stream, callback) {
+ var oldStream = this.localAudio.getOriginalStream();
+ var newStream = this.rtcUtils.createStream(stream);
+ this.localAudio = this.createLocalStream(newStream, "audio", true);
+ // Stop the stream to trigger onended event for old stream
+ oldStream.stop();
+ APP.xmpp.switchStreams(newStream, oldStream, callback, true);
+ },
+ isVideoMuted: function (jid) {
+ if (jid === APP.xmpp.myJid()) {
+ var localVideo = APP.RTC.localVideo;
+ return (!localVideo || localVideo.isMuted());
+ } else {
+ if (!APP.RTC.remoteStreams[jid] ||
+ !APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
+ return null;
+ }
+ return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted;
+ }
+ },
+ setVideoMute: function (mute, callback, options) {
+ if (!this.localVideo)
+ return;
+
+ if (mute == APP.RTC.localVideo.isMuted())
+ {
+ APP.xmpp.sendVideoInfoPresence(mute);
+ if (callback)
+ callback(mute);
+ }
+ else
+ {
+ APP.RTC.localVideo.setMute(mute);
+ APP.xmpp.setVideoMute(
+ mute,
+ callback,
+ options);
+ }
+ },
+ setDeviceAvailability: function (devices) {
+ if(!devices)
+ return;
+ if(devices.audio === true || devices.audio === false)
+ this.devices.audio = devices.audio;
+ if(devices.video === true || devices.video === false)
+ this.devices.video = devices.video;
+ eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, this.devices);
+ }
+};
+
+module.exports = RTC;
+
+},{"../../service/RTC/MediaStreamTypes":159,"../../service/RTC/RTCEvents.js":160,"../../service/RTC/StreamEventTypes.js":162,"../../service/UI/UIEvents":163,"../../service/desktopsharing/DesktopSharingEventTypes":166,"../../service/xmpp/XMPPEvents":169,"./DataChannels":4,"./LocalStream.js":5,"./MediaStream.js":6,"./RTCBrowserType":8,"./RTCUtils.js":9,"events":170}],8:[function(require,module,exports){
+
+var currentBrowser;
+
+var browserVersion;
+
+var isAndroid;
+
+var RTCBrowserType = {
+
+ RTC_BROWSER_CHROME: "rtc_browser.chrome",
+
+ RTC_BROWSER_OPERA: "rtc_browser.opera",
+
+ RTC_BROWSER_FIREFOX: "rtc_browser.firefox",
+
+ RTC_BROWSER_IEXPLORER: "rtc_browser.iexplorer",
+
+ RTC_BROWSER_SAFARI: "rtc_browser.safari",
+
+ getBrowserType: function () {
+ return currentBrowser;
+ },
+
+ isChrome: function () {
+ return currentBrowser === RTCBrowserType.RTC_BROWSER_CHROME;
+ },
+
+ isOpera: function () {
+ return currentBrowser === RTCBrowserType.RTC_BROWSER_OPERA;
+ },
+ isFirefox: function () {
+ return currentBrowser === RTCBrowserType.RTC_BROWSER_FIREFOX;
+ },
+
+ isIExplorer: function () {
+ return currentBrowser === RTCBrowserType.RTC_BROWSER_IEXPLORER;
+ },
+
+ isSafari: function () {
+ return currentBrowser === RTCBrowserType.RTC_BROWSER_SAFARI;
+ },
+ isTemasysPluginUsed: function () {
+ return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari();
+ },
+ getFirefoxVersion: function () {
+ return RTCBrowserType.isFirefox() ? browserVersion : null;
+ },
+
+ getChromeVersion: function () {
+ return RTCBrowserType.isChrome() ? browserVersion : null;
+ },
+
+ usesPlanB: function() {
+ return RTCBrowserType.isChrome() || RTCBrowserType.isOpera() ||
+ RTCBrowserType.isTemasysPluginUsed();
+ },
+
+ usesUnifiedPlan: function() {
+ return RTCBrowserType.isFirefox();
+ },
+
+ /**
+ * Whether the browser is running on an android device.
+ */
+ isAndroid: function() {
+ return isAndroid;
+ }
+
+ // Add version getters for other browsers when needed
+};
+
+// detectOpera() must be called before detectChrome() !!!
+// otherwise Opera wil be detected as Chrome
+function detectChrome() {
+ if (navigator.webkitGetUserMedia) {
+ currentBrowser = RTCBrowserType.RTC_BROWSER_CHROME;
+ 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("This appears to be Chrome, ver: " + ver);
+ return ver;
+ }
+ return null;
+}
+
+function detectOpera() {
+ var userAgent = navigator.userAgent;
+ if (userAgent.match(/Opera|OPR/)) {
+ currentBrowser = RTCBrowserType.RTC_BROWSER_OPERA;
+ var version = userAgent.match(/(Opera|OPR) ?\/?(\d+)\.?/)[2];
+ console.info("This appears to be Opera, ver: " + version);
+ return version;
+ }
+ return null;
+}
+
+function detectFirefox() {
+ if (navigator.mozGetUserMedia) {
+ currentBrowser = RTCBrowserType.RTC_BROWSER_FIREFOX;
+ var version = parseInt(
+ navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
+ console.log('This appears to be Firefox, ver: ' + version);
+ return version;
+ }
+ return null;
+}
+
+function detectSafari() {
+ if ((/^((?!chrome).)*safari/i.test(navigator.userAgent))) {
+ currentBrowser = RTCBrowserType.RTC_BROWSER_SAFARI;
+ console.info("This appears to be Safari");
+ // FIXME detect Safari version when needed
+ return 1;
+ }
+ return null;
+}
+
+function detectIE() {
+ var version;
+ var ua = window.navigator.userAgent;
+
+ var msie = ua.indexOf('MSIE ');
+ if (msie > 0) {
+ // IE 10 or older => return version number
+ version = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
+ }
+
+ var trident = ua.indexOf('Trident/');
+ if (!version && trident > 0) {
+ // IE 11 => return version number
+ var rv = ua.indexOf('rv:');
+ version = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
+ }
+
+ var edge = ua.indexOf('Edge/');
+ if (!version && edge > 0) {
+ // IE 12 => return version number
+ version = parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
+ }
+
+ if (version) {
+ currentBrowser = RTCBrowserType.RTC_BROWSER_IEXPLORER;
+ console.info("This appears to be IExplorer, ver: " + version);
+ }
+ return version;
+}
+
+function detectBrowser() {
+ var version;
+ var detectors = [
+ detectOpera,
+ detectChrome,
+ detectFirefox,
+ detectIE,
+ detectSafari
+ ];
+ // Try all browser detectors
+ for (var i = 0; i < detectors.length; i++) {
+ version = detectors[i]();
+ if (version)
+ return version;
+ }
+ console.error("Failed to detect browser type");
+ return undefined;
+}
+
+browserVersion = detectBrowser();
+isAndroid = navigator.userAgent.indexOf('Android') != -1;
+
+module.exports = RTCBrowserType;
+},{}],9:[function(require,module,exports){
+/* global config, require, attachMediaStream, getUserMedia */
+var RTCBrowserType = require("./RTCBrowserType");
+var Resolutions = require("../../service/RTC/Resolutions");
+var AdapterJS = require("./adapter.screenshare");
+var SDPUtil = require("../xmpp/SDPUtil");
+
+var currentResolution = null;
+
+function getPreviousResolution(resolution) {
+ if(!Resolutions[resolution])
+ return null;
+ var order = Resolutions[resolution].order;
+ var res = null;
+ var resName = null;
+ for(var i in Resolutions) {
+ var tmp = Resolutions[i];
+ if(res == null || (res.order < tmp.order && tmp.order < order)) {
+ resName = i;
+ res = tmp;
+ }
+ }
+ return resName;
+}
+
+function setResolutionConstraints(constraints, resolution) {
+ var isAndroid = RTCBrowserType.isAndroid();
+
+ if (Resolutions[resolution]) {
+ constraints.video.mandatory.minWidth = Resolutions[resolution].width;
+ constraints.video.mandatory.minHeight = Resolutions[resolution].height;
+ }
+ else if (isAndroid) {
+ // FIXME can't remember if the purpose of this was to always request
+ // low resolution on Android ? if yes it should be moved up front
+ constraints.video.mandatory.minWidth = 320;
+ constraints.video.mandatory.minHeight = 240;
+ constraints.video.mandatory.maxFrameRate = 15;
+ }
+
+ if (constraints.video.mandatory.minWidth)
+ constraints.video.mandatory.maxWidth =
+ constraints.video.mandatory.minWidth;
+ if (constraints.video.mandatory.minHeight)
+ constraints.video.mandatory.maxHeight =
+ constraints.video.mandatory.minHeight;
+}
+
+function getConstraints(um, resolution, bandwidth, fps, desktopStream) {
+ var constraints = {audio: false, video: false};
+
+ if (um.indexOf('video') >= 0) {
+ // same behaviour as true
+ constraints.video = { mandatory: {}, optional: [] };
+
+ constraints.video.optional.push({ googLeakyBucket: true });
+
+ setResolutionConstraints(constraints, resolution);
+ }
+ if (um.indexOf('audio') >= 0) {
+ if (!RTCBrowserType.isFirefox()) {
+ // same behaviour as true
+ constraints.audio = { mandatory: {}, optional: []};
+ // if it is good enough for hangouts...
+ constraints.audio.optional.push(
+ {googEchoCancellation: true},
+ {googAutoGainControl: true},
+ {googNoiseSupression: true},
+ {googHighpassFilter: true},
+ {googNoisesuppression2: true},
+ {googEchoCancellation2: true},
+ {googAutoGainControl2: true}
+ );
+ } else {
+ constraints.audio = true;
+ }
+ }
+ if (um.indexOf('screen') >= 0) {
+ if (RTCBrowserType.isChrome()) {
+ constraints.video = {
+ mandatory: {
+ chromeMediaSource: "screen",
+ googLeakyBucket: true,
+ maxWidth: window.screen.width,
+ maxHeight: window.screen.height,
+ maxFrameRate: 3
+ },
+ optional: []
+ };
+ } else if (RTCBrowserType.isTemasysPluginUsed()) {
+ constraints.video = {
+ optional: [
+ {
+ sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
+ }
+ ]
+ };
+ } else {
+ console.error(
+ "'screen' WebRTC media source is supported only in Chrome" +
+ " and with Temasys plugin");
+ }
+ }
+ if (um.indexOf('desktop') >= 0) {
+ constraints.video = {
+ mandatory: {
+ chromeMediaSource: "desktop",
+ chromeMediaSourceId: desktopStream,
+ googLeakyBucket: true,
+ maxWidth: window.screen.width,
+ maxHeight: window.screen.height,
+ maxFrameRate: 3
+ },
+ optional: []
+ };
+ }
+
+ if (bandwidth) {
+ if (!constraints.video) {
+ //same behaviour as true
+ constraints.video = {mandatory: {}, optional: []};
+ }
+ constraints.video.optional.push({bandwidth: bandwidth});
+ }
+ if (fps) {
+ // for some cameras it might be necessary to request 30fps
+ // so they choose 30fps mjpg over 10fps yuy2
+ if (!constraints.video) {
+ // same behaviour as true;
+ constraints.video = {mandatory: {}, optional: []};
+ }
+ constraints.video.mandatory.minFrameRate = fps;
+ }
+
+ // we turn audio for both audio and video tracks, the fake audio & video seems to work
+ // only when enabled in one getUserMedia call, we cannot get fake audio separate by fake video
+ // this later can be a problem with some of the tests
+ if(RTCBrowserType.isFirefox() && config.firefox_fake_device)
+ {
+ constraints.audio = true;
+ constraints.fake = true;
+ }
+
+ return constraints;
+}
+
+
+function RTCUtils(RTCService, onTemasysPluginReady)
+{
+ var self = this;
+ this.service = RTCService;
+ if (RTCBrowserType.isFirefox()) {
+ var FFversion = RTCBrowserType.getFirefoxVersion();
+ if (FFversion >= 40) {
+ this.peerconnection = mozRTCPeerConnection;
+ this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
+ this.pc_constraints = {};
+ this.attachMediaStream = function (element, stream) {
+ // srcObject is being standardized and FF will eventually
+ // support that unprefixed. FF also supports the
+ // "element.src = URL.createObjectURL(...)" combo, but that
+ // will be deprecated in favour of srcObject.
+ //
+ // https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
+ // https://github.com/webrtc/samples/issues/302
+ if(!element[0])
+ return;
+ element[0].mozSrcObject = stream;
+ element[0].play();
+ };
+ this.getStreamID = function (stream) {
+ var id = stream.id;
+ if (!id) {
+ var tracks = stream.getVideoTracks();
+ if (!tracks || tracks.length === 0) {
+ tracks = stream.getAudioTracks();
+ }
+ id = tracks[0].id;
+ }
+ return SDPUtil.filter_special_chars(id);
+ };
+ this.getVideoSrc = function (element) {
+ if(!element)
+ return null;
+ return element.mozSrcObject;
+ };
+ this.setVideoSrc = function (element, src) {
+ if(element)
+ element.mozSrcObject = src;
+ };
+ RTCSessionDescription = mozRTCSessionDescription;
+ RTCIceCandidate = mozRTCIceCandidate;
+ } else {
+ console.error(
+ "Firefox version too old: " + FFversion + ". Required >= 40.");
+ window.location.href = 'unsupported_browser.html';
+ return;
+ }
+
+ } else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
+ this.peerconnection = webkitRTCPeerConnection;
+ this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+ this.attachMediaStream = function (element, stream) {
+ element.attr('src', webkitURL.createObjectURL(stream));
+ };
+ this.getStreamID = function (stream) {
+ // streams from FF endpoints have the characters '{' and '}'
+ // that make jQuery choke.
+ return SDPUtil.filter_special_chars(stream.id);
+ };
+ this.getVideoSrc = function (element) {
+ if(!element)
+ return null;
+ return element.getAttribute("src");
+ };
+ this.setVideoSrc = function (element, src) {
+ if(element)
+ element.setAttribute("src", src);
+ };
+ // DTLS should now be enabled by default but..
+ this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
+ if (RTCBrowserType.isAndroid()) {
+ this.pc_constraints = {}; // disable DTLS on Android
+ }
+ if (!webkitMediaStream.prototype.getVideoTracks) {
+ webkitMediaStream.prototype.getVideoTracks = function () {
+ return this.videoTracks;
+ };
+ }
+ if (!webkitMediaStream.prototype.getAudioTracks) {
+ webkitMediaStream.prototype.getAudioTracks = function () {
+ return this.audioTracks;
+ };
+ }
+ }
+ // Detect IE/Safari
+ else if (RTCBrowserType.isTemasysPluginUsed()) {
+
+ //AdapterJS.WebRTCPlugin.setLogLevel(
+ // AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
+
+ AdapterJS.webRTCReady(function (isPlugin) {
+
+ self.peerconnection = RTCPeerConnection;
+ self.getUserMedia = getUserMedia;
+ self.attachMediaStream = function (elSel, stream) {
+
+ if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
+ return;
+ }
+
+ attachMediaStream(elSel[0], stream);
+ };
+ self.getStreamID = function (stream) {
+ var id = SDPUtil.filter_special_chars(stream.label);
+ return id;
+ };
+ self.getVideoSrc = function (element) {
+ if (!element) {
+ console.warn("Attempt to get video SRC of null element");
+ return null;
+ }
+ var children = element.children;
+ for (var i = 0; i !== children.length; ++i) {
+ if (children[i].name === 'streamId') {
+ return children[i].value;
+ }
+ }
+ //console.info(element.id + " SRC: " + src);
+ return null;
+ };
+ self.setVideoSrc = function (element, src) {
+ //console.info("Set video src: ", element, src);
+ if (!src) {
+ console.warn("Not attaching video stream, 'src' is null");
+ return;
+ }
+ AdapterJS.WebRTCPlugin.WaitForPluginReady();
+ var stream = AdapterJS.WebRTCPlugin.plugin
+ .getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
+ attachMediaStream(element, stream);
+ };
+
+ onTemasysPluginReady(isPlugin);
+ });
+ } else {
+ try {
+ console.log('Browser does not appear to be WebRTC-capable');
+ } catch (e) { }
+ window.location.href = 'unsupported_browser.html';
+ }
+}
+
+
+RTCUtils.prototype.getUserMediaWithConstraints = function(
+ um, success_callback, failure_callback, resolution,bandwidth, fps,
+ desktopStream) {
+ currentResolution = resolution;
+
+ var constraints = getConstraints(
+ um, resolution, bandwidth, fps, desktopStream);
+
+ console.info("Get media constraints", constraints);
+
+ var self = this;
+
+ try {
+ this.getUserMedia(constraints,
+ function (stream) {
+ console.log('onUserMediaSuccess');
+ self.setAvailableDevices(um, true);
+ success_callback(stream);
+ },
+ function (error) {
+ self.setAvailableDevices(um, false);
+ console.warn('Failed to get access to local media. Error ',
+ error, constraints);
+ if (failure_callback) {
+ failure_callback(error);
+ }
+ });
+ } catch (e) {
+ console.error('GUM failed: ', e);
+ if(failure_callback) {
+ failure_callback(e);
+ }
+ }
+};
+
+RTCUtils.prototype.setAvailableDevices = function (um, available) {
+ var devices = {};
+ if(um.indexOf("video") != -1) {
+ devices.video = available;
+ }
+ if(um.indexOf("audio") != -1) {
+ devices.audio = available;
+ }
+ this.service.setDeviceAvailability(devices);
+};
+
+/**
+ * We ask for audio and video combined stream in order to get permissions and
+ * not to ask twice.
+ */
+RTCUtils.prototype.obtainAudioAndVideoPermissions =
+ function(devices, callback, usageOptions)
+{
+ var self = this;
+ // Get AV
+
+ var successCallback = function (stream) {
+ if(callback)
+ callback(stream, usageOptions);
+ else
+ self.successCallback(stream, usageOptions);
+ };
+
+ if(!devices)
+ devices = ['audio', 'video'];
+
+ var newDevices = [];
+
+
+ if(usageOptions)
+ for(var i = 0; i < devices.length; i++) {
+ var device = devices[i];
+ if(usageOptions[device] === true)
+ newDevices.push(device);
+ }
+ else
+ newDevices = devices;
+
+ if(newDevices.length === 0) {
+ successCallback();
+ return;
+ }
+
+ if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
+
+ // With FF/IE we can't split the stream into audio and video because FF
+ // doesn't support media stream constructors. So, we need to get the
+ // audio stream separately from the video stream using two distinct GUM
+ // calls. Not very user friendly :-( but we don't have many other
+ // options neither.
+ //
+ // Note that we pack those 2 streams in a single object and pass it to
+ // the successCallback method.
+ var obtainVideo = function (audioStream) {
+ self.getUserMediaWithConstraints(
+ ['video'],
+ function (videoStream) {
+ return successCallback({
+ audioStream: audioStream,
+ videoStream: videoStream
+ });
+ },
+ function (error) {
+ console.error(
+ 'failed to obtain video stream - stop', error);
+ self.errorCallback(error);
+ },
+ config.resolution || '360');
+ };
+ var obtainAudio = function () {
+ self.getUserMediaWithConstraints(
+ ['audio'],
+ function (audioStream) {
+ if (newDevices.indexOf('video') !== -1)
+ obtainVideo(audioStream);
+ },
+ function (error) {
+ console.error(
+ 'failed to obtain audio stream - stop', error);
+ self.errorCallback(error);
+ }
+ );
+ };
+ if (newDevices.indexOf('audio') !== -1) {
+ obtainAudio();
+ } else {
+ obtainVideo(null);
+ }
+ } else {
+ this.getUserMediaWithConstraints(
+ newDevices,
+ function (stream) {
+ successCallback(stream);
+ },
+ function (error) {
+ self.errorCallback(error);
+ },
+ config.resolution || '360');
+ }
+};
+
+RTCUtils.prototype.successCallback = function (stream, usageOptions) {
+ // If this is FF or IE, the stream parameter is *not* a MediaStream object,
+ // it's an object with two properties: audioStream, videoStream.
+ if (stream && stream.getAudioTracks && stream.getVideoTracks)
+ console.log('got', stream, stream.getAudioTracks().length,
+ stream.getVideoTracks().length);
+ this.handleLocalStream(stream, usageOptions);
+};
+
+RTCUtils.prototype.errorCallback = function (error) {
+ var self = this;
+ console.error('failed to obtain audio/video stream - trying audio only', error);
+ var resolution = getPreviousResolution(currentResolution);
+ if(typeof error == "object" && error.constraintName && error.name
+ && (error.name == "ConstraintNotSatisfiedError" ||
+ error.name == "OverconstrainedError") &&
+ (error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
+ error.constraintName == "minHeight" || error.constraintName == "maxHeight")
+ && resolution != null)
+ {
+ self.getUserMediaWithConstraints(['audio', 'video'],
+ function (stream) {
+ return self.successCallback(stream);
+ }, function (error) {
+ return self.errorCallback(error);
+ }, resolution);
+ }
+ else {
+ self.getUserMediaWithConstraints(
+ ['audio'],
+ function (stream) {
+ return self.successCallback(stream);
+ },
+ function (error) {
+ console.error('failed to obtain audio/video stream - stop',
+ error);
+ return self.successCallback(null);
+ }
+ );
+ }
+};
+
+RTCUtils.prototype.handleLocalStream = function(stream, usageOptions) {
+ // If this is FF, the stream parameter is *not* a MediaStream object, it's
+ // an object with two properties: audioStream, videoStream.
+ var audioStream, videoStream;
+ if(window.webkitMediaStream)
+ {
+ audioStream = new webkitMediaStream();
+ videoStream = new webkitMediaStream();
+ if(stream) {
+ var audioTracks = stream.getAudioTracks();
+
+ for (var i = 0; i < audioTracks.length; i++) {
+ audioStream.addTrack(audioTracks[i]);
+ }
+
+ var videoTracks = stream.getVideoTracks();
+
+ for (i = 0; i < videoTracks.length; i++) {
+ videoStream.addTrack(videoTracks[i]);
+ }
+ }
+ }
+ else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
+ { // Firefox and Temasys plugin
+ if (stream && stream.audioStream)
+ audioStream = stream.audioStream;
+ else
+ audioStream = new DummyMediaStream("dummyAudio");
+
+ if (stream && stream.videoStream)
+ videoStream = stream.videoStream;
+ else
+ videoStream = new DummyMediaStream("dummyVideo");
+ }
+
+ var audioMuted = (usageOptions && usageOptions.audio === false),
+ videoMuted = (usageOptions && usageOptions.video === false);
+
+ var audioGUM = (!usageOptions || usageOptions.audio !== false),
+ videoGUM = (!usageOptions || usageOptions.video !== false);
+
+
+ this.service.createLocalStream(audioStream, "audio", null, null,
+ audioMuted, audioGUM);
+
+ this.service.createLocalStream(videoStream, "video", null, 'camera',
+ videoMuted, videoGUM);
+};
+
+function DummyMediaStream(id) {
+ this.id = id;
+ this.label = id;
+ this.stop = function() { };
+ this.getAudioTracks = function() { return []; };
+ this.getVideoTracks = function() { return []; };
+}
+
+RTCUtils.prototype.createStream = function(stream, isVideo) {
+ var newStream = null;
+ if (window.webkitMediaStream) {
+ newStream = new webkitMediaStream();
+ if (newStream) {
+ var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
+
+ for (var i = 0; i < tracks.length; i++) {
+ newStream.addTrack(tracks[i]);
+ }
+ }
+
+ }
+ else {
+ // FIXME: this is duplicated with 'handleLocalStream' !!!
+ if (stream) {
+ newStream = stream;
+ } else {
+ newStream =
+ new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
+ }
+ }
+
+ return newStream;
+};
+
+module.exports = RTCUtils;
+
+},{"../../service/RTC/Resolutions":161,"../xmpp/SDPUtil":58,"./RTCBrowserType":8,"./adapter.screenshare":10}],10:[function(require,module,exports){
+/*! adapterjs - custom version from - 2015-08-18 */
+
+// Adapter's interface.
+var AdapterJS = AdapterJS || {};
+
+// Browserify compatibility
+if(typeof exports !== 'undefined') {
+ module.exports = AdapterJS;
+}
+
+AdapterJS.options = AdapterJS.options || {};
+
+// uncomment to get virtual webcams
+// AdapterJS.options.getAllCams = true;
+
+// uncomment to prevent the install prompt when the plugin in not yet installed
+// AdapterJS.options.hidePluginInstallPrompt = true;
+
+// AdapterJS version
+AdapterJS.VERSION = '0.12.0';
+
+// This function will be called when the WebRTC API is ready to be used
+// Whether it is the native implementation (Chrome, Firefox, Opera) or
+// the plugin
+// You may Override this function to synchronise the start of your application
+// with the WebRTC API being ready.
+// If you decide not to override use this synchronisation, it may result in
+// an extensive CPU usage on the plugin start (once per tab loaded)
+// Params:
+// - isUsingPlugin: true is the WebRTC plugin is being used, false otherwise
+//
+AdapterJS.onwebrtcready = AdapterJS.onwebrtcready || function(isUsingPlugin) {
+ // The WebRTC API is ready.
+ // Override me and do whatever you want here
+};
+
+// Sets a callback function to be called when the WebRTC interface is ready.
+// The first argument is the function to callback.\
+// Throws an error if the first argument is not a function
+AdapterJS.webRTCReady = function (callback) {
+ if (typeof callback !== 'function') {
+ throw new Error('Callback provided is not a function');
+ }
+
+ if (true === AdapterJS.onwebrtcreadyDone) {
+ // All WebRTC interfaces are ready, just call the callback
+ callback(null !== AdapterJS.WebRTCPlugin.plugin);
+ } else {
+ // will be triggered automatically when your browser/plugin is ready.
+ AdapterJS.onwebrtcready = callback;
+ }
+};
+
+// Plugin namespace
+AdapterJS.WebRTCPlugin = AdapterJS.WebRTCPlugin || {};
+
+// The object to store plugin information
+AdapterJS.WebRTCPlugin.pluginInfo = {
+ prefix : 'Tem',
+ plugName : 'TemWebRTCPlugin',
+ pluginId : 'plugin0',
+ type : 'application/x-temwebrtcplugin',
+ onload : '__TemWebRTCReady0',
+ portalLink : 'http://skylink.io/plugin/',
+ downloadLink : null, //set below
+ companyName: 'Temasys'
+};
+if(!!navigator.platform.match(/^Mac/i)) {
+ AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1n77hco';
+}
+else if(!!navigator.platform.match(/^Win/i)) {
+ AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1kkS4FN';
+}
+
+AdapterJS.WebRTCPlugin.TAGS = {
+ NONE : 'none',
+ AUDIO : 'audio',
+ VIDEO : 'video'
+};
+
+// Unique identifier of each opened page
+AdapterJS.WebRTCPlugin.pageId = Math.random().toString(36).slice(2);
+
+// Use this whenever you want to call the plugin.
+AdapterJS.WebRTCPlugin.plugin = null;
+
+// Set log level for the plugin once it is ready.
+// The different values are
+// This is an asynchronous function that will run when the plugin is ready
+AdapterJS.WebRTCPlugin.setLogLevel = null;
+
+// Defines webrtc's JS interface according to the plugin's implementation.
+// Define plugin Browsers as WebRTC Interface.
+AdapterJS.WebRTCPlugin.defineWebRTCInterface = null;
+
+// This function detects whether or not a plugin is installed.
+// Checks if Not IE (firefox, for example), else if it's IE,
+// we're running IE and do something. If not it is not supported.
+AdapterJS.WebRTCPlugin.isPluginInstalled = null;
+
+ // Lets adapter.js wait until the the document is ready before injecting the plugin
+AdapterJS.WebRTCPlugin.pluginInjectionInterval = null;
+
+// Inject the HTML DOM object element into the page.
+AdapterJS.WebRTCPlugin.injectPlugin = null;
+
+// States of readiness that the plugin goes through when
+// being injected and stated
+AdapterJS.WebRTCPlugin.PLUGIN_STATES = {
+ NONE : 0, // no plugin use
+ INITIALIZING : 1, // Detected need for plugin
+ INJECTING : 2, // Injecting plugin
+ INJECTED: 3, // Plugin element injected but not usable yet
+ READY: 4 // Plugin ready to be used
+};
+
+// Current state of the plugin. You cannot use the plugin before this is
+// equal to AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY
+AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.NONE;
+
+// True is AdapterJS.onwebrtcready was already called, false otherwise
+// Used to make sure AdapterJS.onwebrtcready is only called once
+AdapterJS.onwebrtcreadyDone = false;
+
+// Log levels for the plugin.
+// To be set by calling AdapterJS.WebRTCPlugin.setLogLevel
+/*
+Log outputs are prefixed in some cases.
+ INFO: Information reported by the plugin.
+ ERROR: Errors originating from within the plugin.
+ WEBRTC: Error originating from within the libWebRTC library
+*/
+// From the least verbose to the most verbose
+AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS = {
+ NONE : 'NONE',
+ ERROR : 'ERROR',
+ WARNING : 'WARNING',
+ INFO: 'INFO',
+ VERBOSE: 'VERBOSE',
+ SENSITIVE: 'SENSITIVE'
+};
+
+// Does a waiting check before proceeding to load the plugin.
+AdapterJS.WebRTCPlugin.WaitForPluginReady = null;
+
+// This methid will use an interval to wait for the plugin to be ready.
+AdapterJS.WebRTCPlugin.callWhenPluginReady = null;
+
+// !!!! WARNING: DO NOT OVERRIDE THIS FUNCTION. !!!
+// This function will be called when plugin is ready. It sends necessary
+// details to the plugin.
+// The function will wait for the document to be ready and the set the
+// plugin state to AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY,
+// indicating that it can start being requested.
+// This function is not in the IE/Safari condition brackets so that
+// TemPluginLoaded function might be called on Chrome/Firefox.
+// This function is the only private function that is not encapsulated to
+// allow the plugin method to be called.
+__TemWebRTCReady0 = function () {
+ if (document.readyState === 'complete') {
+ AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY;
+
+ AdapterJS.maybeThroughWebRTCReady();
+ } else {
+ AdapterJS.WebRTCPlugin.documentReadyInterval = setInterval(function () {
+ if (document.readyState === 'complete') {
+ // TODO: update comments, we wait for the document to be ready
+ clearInterval(AdapterJS.WebRTCPlugin.documentReadyInterval);
+ AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY;
+
+ AdapterJS.maybeThroughWebRTCReady();
+ }
+ }, 100);
+ }
+};
+
+AdapterJS.maybeThroughWebRTCReady = function() {
+ if (!AdapterJS.onwebrtcreadyDone) {
+ AdapterJS.onwebrtcreadyDone = true;
+
+ if (typeof(AdapterJS.onwebrtcready) === 'function') {
+ AdapterJS.onwebrtcready(AdapterJS.WebRTCPlugin.plugin !== null);
+ }
+ }
+};
+
+// Text namespace
+AdapterJS.TEXT = {
+ PLUGIN: {
+ REQUIRE_INSTALLATION: 'This website requires you to install a WebRTC-enabling plugin ' +
+ 'to work on this browser.',
+ NOT_SUPPORTED: 'Your browser does not support WebRTC.',
+ BUTTON: 'Install Now'
+ },
+ REFRESH: {
+ REQUIRE_REFRESH: 'Please refresh page',
+ BUTTON: 'Refresh Page'
+ }
+};
+
+// The result of ice connection states.
+// - starting: Ice connection is starting.
+// - checking: Ice connection is checking.
+// - connected Ice connection is connected.
+// - completed Ice connection is connected.
+// - done Ice connection has been completed.
+// - disconnected Ice connection has been disconnected.
+// - failed Ice connection has failed.
+// - closed Ice connection is closed.
+AdapterJS._iceConnectionStates = {
+ starting : 'starting',
+ checking : 'checking',
+ connected : 'connected',
+ completed : 'connected',
+ done : 'completed',
+ disconnected : 'disconnected',
+ failed : 'failed',
+ closed : 'closed'
+};
+
+//The IceConnection states that has been fired for each peer.
+AdapterJS._iceConnectionFiredStates = [];
+
+
+// Check if WebRTC Interface is defined.
+AdapterJS.isDefined = null;
+
+// This function helps to retrieve the webrtc detected browser information.
+// This sets:
+// - webrtcDetectedBrowser: The browser agent name.
+// - webrtcDetectedVersion: The browser version.
+// - webrtcDetectedType: The types of webRTC support.
+// - 'moz': Mozilla implementation of webRTC.
+// - 'webkit': WebKit implementation of webRTC.
+// - 'plugin': Using the plugin implementation.
+AdapterJS.parseWebrtcDetectedBrowser = function () {
+ var hasMatch, checkMatch = navigator.userAgent.match(
+ /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
+ if (/trident/i.test(checkMatch[1])) {
+ hasMatch = /\brv[ :]+(\d+)/g.exec(navigator.userAgent) || [];
+ webrtcDetectedBrowser = 'IE';
+ webrtcDetectedVersion = parseInt(hasMatch[1] || '0', 10);
+ } else if (checkMatch[1] === 'Chrome') {
+ hasMatch = navigator.userAgent.match(/\bOPR\/(\d+)/);
+ if (hasMatch !== null) {
+ webrtcDetectedBrowser = 'opera';
+ webrtcDetectedVersion = parseInt(hasMatch[1], 10);
+ }
+ }
+ if (navigator.userAgent.indexOf('Safari')) {
+ if (typeof InstallTrigger !== 'undefined') {
+ webrtcDetectedBrowser = 'firefox';
+ } else if (/*@cc_on!@*/ false || !!document.documentMode) {
+ webrtcDetectedBrowser = 'IE';
+ } else if (
+ Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0) {
+ webrtcDetectedBrowser = 'safari';
+ } else if (!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
+ webrtcDetectedBrowser = 'opera';
+ } else if (!!window.chrome) {
+ webrtcDetectedBrowser = 'chrome';
+ }
+ }
+ if (!webrtcDetectedBrowser) {
+ webrtcDetectedVersion = checkMatch[1];
+ }
+ if (!webrtcDetectedVersion) {
+ try {
+ checkMatch = (checkMatch[2]) ? [checkMatch[1], checkMatch[2]] :
+ [navigator.appName, navigator.appVersion, '-?'];
+ if ((hasMatch = navigator.userAgent.match(/version\/(\d+)/i)) !== null) {
+ checkMatch.splice(1, 1, hasMatch[1]);
+ }
+ webrtcDetectedVersion = parseInt(checkMatch[1], 10);
+ } catch (error) { }
+ }
+};
+
+// To fix configuration as some browsers does not support
+// the 'urls' attribute.
+AdapterJS.maybeFixConfiguration = function (pcConfig) {
+ if (pcConfig === null) {
+ return;
+ }
+ for (var i = 0; i < pcConfig.iceServers.length; i++) {
+ if (pcConfig.iceServers[i].hasOwnProperty('urls')) {
+ pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls;
+ delete pcConfig.iceServers[i].urls;
+ }
+ }
+};
+
+AdapterJS.addEvent = function(elem, evnt, func) {
+ if (elem.addEventListener) { // W3C DOM
+ elem.addEventListener(evnt, func, false);
+ } else if (elem.attachEvent) {// OLD IE DOM
+ elem.attachEvent('on'+evnt, func);
+ } else { // No much to do
+ elem[evnt] = func;
+ }
+};
+
+AdapterJS.renderNotificationBar = function (text, buttonText, buttonLink, openNewTab, displayRefreshBar) {
+ // only inject once the page is ready
+ if (document.readyState !== 'complete') {
+ return;
+ }
+
+ var w = window;
+ var i = document.createElement('iframe');
+ i.style.position = 'fixed';
+ i.style.top = '-41px';
+ i.style.left = 0;
+ i.style.right = 0;
+ i.style.width = '100%';
+ i.style.height = '40px';
+ i.style.backgroundColor = '#ffffe1';
+ i.style.border = 'none';
+ i.style.borderBottom = '1px solid #888888';
+ i.style.zIndex = '9999999';
+ if(typeof i.style.webkitTransition === 'string') {
+ i.style.webkitTransition = 'all .5s ease-out';
+ } else if(typeof i.style.transition === 'string') {
+ i.style.transition = 'all .5s ease-out';
+ }
+ document.body.appendChild(i);
+ c = (i.contentWindow) ? i.contentWindow :
+ (i.contentDocument.document) ? i.contentDocument.document : i.contentDocument;
+ c.document.open();
+ c.document.write('' + text + ' ');
+ if(buttonText && buttonLink) {
+ c.document.write('' + buttonText + ' Cancel ');
+ c.document.close();
+
+ AdapterJS.addEvent(c.document.getElementById('okay'), 'click', function(e) {
+ if (!!displayRefreshBar) {
+ AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION ?
+ AdapterJS.TEXT.EXTENSION.REQUIRE_REFRESH : AdapterJS.TEXT.REFRESH.REQUIRE_REFRESH,
+ AdapterJS.TEXT.REFRESH.BUTTON, 'javascript:location.reload()');
+ }
+ window.open(buttonLink, !!openNewTab ? '_blank' : '_top');
+
+ e.preventDefault();
+ try {
+ event.cancelBubble = true;
+ } catch(error) { }
+
+ var pluginInstallInterval = setInterval(function(){
+ if(! isIE) {
+ navigator.plugins.refresh(false);
+ }
+ AdapterJS.WebRTCPlugin.isPluginInstalled(
+ AdapterJS.WebRTCPlugin.pluginInfo.prefix,
+ AdapterJS.WebRTCPlugin.pluginInfo.plugName,
+ AdapterJS.WebRTCPlugin.defineWebRTCInterface,
+ function() {
+ clearInterval(pluginInstallInterval);
+ AdapterJS.WebRTCPlugin.defineWebRTCInterface();
+ },
+ function() {
+ //Does nothing because not used here
+ });
+ } , 500);
+ });
+ } else {
+ c.document.close();
+ }
+ AdapterJS.addEvent(c.document, 'click', function() {
+ w.document.body.removeChild(i);
+ });
+ setTimeout(function() {
+ if(typeof i.style.webkitTransform === 'string') {
+ i.style.webkitTransform = 'translateY(40px)';
+ } else if(typeof i.style.transform === 'string') {
+ i.style.transform = 'translateY(40px)';
+ } else {
+ i.style.top = '0px';
+ }
+ }, 300);
+};
+
+// -----------------------------------------------------------
+// Detected webrtc implementation. Types are:
+// - 'moz': Mozilla implementation of webRTC.
+// - 'webkit': WebKit implementation of webRTC.
+// - 'plugin': Using the plugin implementation.
+webrtcDetectedType = null;
+
+// Detected webrtc datachannel support. Types are:
+// - 'SCTP': SCTP datachannel support.
+// - 'RTP': RTP datachannel support.
+webrtcDetectedDCSupport = null;
+
+// Set the settings for creating DataChannels, MediaStream for
+// Cross-browser compability.
+// - This is only for SCTP based support browsers.
+// the 'urls' attribute.
+checkMediaDataChannelSettings =
+ function (peerBrowserAgent, peerBrowserVersion, callback, constraints) {
+ if (typeof callback !== 'function') {
+ return;
+ }
+ var beOfferer = true;
+ var isLocalFirefox = webrtcDetectedBrowser === 'firefox';
+ // Nightly version does not require MozDontOfferDataChannel for interop
+ var isLocalFirefoxInterop = webrtcDetectedType === 'moz' && webrtcDetectedVersion > 30;
+ var isPeerFirefox = peerBrowserAgent === 'firefox';
+ var isPeerFirefoxInterop = peerBrowserAgent === 'firefox' &&
+ ((peerBrowserVersion) ? (peerBrowserVersion > 30) : false);
+
+ // Resends an updated version of constraints for MozDataChannel to work
+ // If other userAgent is firefox and user is firefox, remove MozDataChannel
+ if ((isLocalFirefox && isPeerFirefox) || (isLocalFirefoxInterop)) {
+ try {
+ delete constraints.mandatory.MozDontOfferDataChannel;
+ } catch (error) {
+ console.error('Failed deleting MozDontOfferDataChannel');
+ console.error(error);
+ }
+ } else if ((isLocalFirefox && !isPeerFirefox)) {
+ constraints.mandatory.MozDontOfferDataChannel = true;
+ }
+ if (!isLocalFirefox) {
+ // temporary measure to remove Moz* constraints in non Firefox browsers
+ for (var prop in constraints.mandatory) {
+ if (constraints.mandatory.hasOwnProperty(prop)) {
+ if (prop.indexOf('Moz') !== -1) {
+ delete constraints.mandatory[prop];
+ }
+ }
+ }
+ }
+ // Firefox (not interopable) cannot offer DataChannel as it will cause problems to the
+ // interopability of the media stream
+ if (isLocalFirefox && !isPeerFirefox && !isLocalFirefoxInterop) {
+ beOfferer = false;
+ }
+ callback(beOfferer, constraints);
+};
+
+// Handles the differences for all browsers ice connection state output.
+// - Tested outcomes are:
+// - Chrome (offerer) : 'checking' > 'completed' > 'completed'
+// - Chrome (answerer) : 'checking' > 'connected'
+// - Firefox (offerer) : 'checking' > 'connected'
+// - Firefox (answerer): 'checking' > 'connected'
+checkIceConnectionState = function (peerId, iceConnectionState, callback) {
+ if (typeof callback !== 'function') {
+ console.warn('No callback specified in checkIceConnectionState. Aborted.');
+ return;
+ }
+ peerId = (peerId) ? peerId : 'peer';
+
+ if (!AdapterJS._iceConnectionFiredStates[peerId] ||
+ iceConnectionState === AdapterJS._iceConnectionStates.disconnected ||
+ iceConnectionState === AdapterJS._iceConnectionStates.failed ||
+ iceConnectionState === AdapterJS._iceConnectionStates.closed) {
+ AdapterJS._iceConnectionFiredStates[peerId] = [];
+ }
+ iceConnectionState = AdapterJS._iceConnectionStates[iceConnectionState];
+ if (AdapterJS._iceConnectionFiredStates[peerId].indexOf(iceConnectionState) < 0) {
+ AdapterJS._iceConnectionFiredStates[peerId].push(iceConnectionState);
+ if (iceConnectionState === AdapterJS._iceConnectionStates.connected) {
+ setTimeout(function () {
+ AdapterJS._iceConnectionFiredStates[peerId]
+ .push(AdapterJS._iceConnectionStates.done);
+ callback(AdapterJS._iceConnectionStates.done);
+ }, 1000);
+ }
+ callback(iceConnectionState);
+ }
+ return;
+};
+
+// Firefox:
+// - Creates iceServer from the url for Firefox.
+// - Create iceServer with stun url.
+// - Create iceServer with turn url.
+// - Ignore the transport parameter from TURN url for FF version <=27.
+// - Return null for createIceServer if transport=tcp.
+// - FF 27 and above supports transport parameters in TURN url,
+// - So passing in the full url to create iceServer.
+// Chrome:
+// - Creates iceServer from the url for Chrome M33 and earlier.
+// - Create iceServer with stun url.
+// - Chrome M28 & above uses below TURN format.
+// Plugin:
+// - Creates Ice Server for Plugin Browsers
+// - If Stun - Create iceServer with stun url.
+// - Else - Create iceServer with turn url
+// - This is a WebRTC Function
+createIceServer = null;
+
+// Firefox:
+// - Creates IceServers for Firefox
+// - Use .url for FireFox.
+// - Multiple Urls support
+// Chrome:
+// - Creates iceServers from the urls for Chrome M34 and above.
+// - .urls is supported since Chrome M34.
+// - Multiple Urls support
+// Plugin:
+// - Creates Ice Servers for Plugin Browsers
+// - Multiple Urls support
+// - This is a WebRTC Function
+createIceServers = null;
+//------------------------------------------------------------
+
+//The RTCPeerConnection object.
+RTCPeerConnection = null;
+
+// Creates RTCSessionDescription object for Plugin Browsers
+RTCSessionDescription = (typeof RTCSessionDescription === 'function') ?
+ RTCSessionDescription : null;
+
+// Creates RTCIceCandidate object for Plugin Browsers
+RTCIceCandidate = (typeof RTCIceCandidate === 'function') ?
+ RTCIceCandidate : null;
+
+// Get UserMedia (only difference is the prefix).
+// Code from Adam Barth.
+getUserMedia = null;
+
+// Attach a media stream to an element.
+attachMediaStream = null;
+
+// Re-attach a media stream to an element.
+reattachMediaStream = null;
+
+
+// Detected browser agent name. Types are:
+// - 'firefox': Firefox browser.
+// - 'chrome': Chrome browser.
+// - 'opera': Opera browser.
+// - 'safari': Safari browser.
+// - 'IE' - Internet Explorer browser.
+webrtcDetectedBrowser = null;
+
+// Detected browser version.
+webrtcDetectedVersion = null;
+
+// Check for browser types and react accordingly
+if (navigator.mozGetUserMedia) {
+ webrtcDetectedBrowser = 'firefox';
+ webrtcDetectedVersion = parseInt(navigator
+ .userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
+ webrtcDetectedType = 'moz';
+ webrtcDetectedDCSupport = 'SCTP';
+
+ RTCPeerConnection = function (pcConfig, pcConstraints) {
+ AdapterJS.maybeFixConfiguration(pcConfig);
+ return new mozRTCPeerConnection(pcConfig, pcConstraints);
+ };
+
+ // The RTCSessionDescription object.
+ RTCSessionDescription = mozRTCSessionDescription;
+ window.RTCSessionDescription = RTCSessionDescription;
+
+ // The RTCIceCandidate object.
+ RTCIceCandidate = mozRTCIceCandidate;
+ window.RTCIceCandidate = RTCIceCandidate;
+
+ window.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
+ navigator.getUserMedia = window.getUserMedia;
+
+ // Shim for MediaStreamTrack.getSources.
+ MediaStreamTrack.getSources = function(successCb) {
+ setTimeout(function() {
+ var infos = [
+ { kind: 'audio', id: 'default', label:'', facing:'' },
+ { kind: 'video', id: 'default', label:'', facing:'' }
+ ];
+ successCb(infos);
+ }, 0);
+ };
+
+ createIceServer = function (url, username, password) {
+ var iceServer = null;
+ var url_parts = url.split(':');
+ if (url_parts[0].indexOf('stun') === 0) {
+ iceServer = { url : url };
+ } else if (url_parts[0].indexOf('turn') === 0) {
+ if (webrtcDetectedVersion < 27) {
+ var turn_url_parts = url.split('?');
+ if (turn_url_parts.length === 1 ||
+ turn_url_parts[1].indexOf('transport=udp') === 0) {
+ iceServer = {
+ url : turn_url_parts[0],
+ credential : password,
+ username : username
+ };
+ }
+ } else {
+ iceServer = {
+ url : url,
+ credential : password,
+ username : username
+ };
+ }
+ }
+ return iceServer;
+ };
+
+ createIceServers = function (urls, username, password) {
+ var iceServers = [];
+ for (i = 0; i < urls.length; i++) {
+ var iceServer = createIceServer(urls[i], username, password);
+ if (iceServer !== null) {
+ iceServers.push(iceServer);
+ }
+ }
+ return iceServers;
+ };
+
+ attachMediaStream = function (element, stream) {
+ element.mozSrcObject = stream;
+ if (stream !== null)
+ element.play();
+
+ return element;
+ };
+
+ reattachMediaStream = function (to, from) {
+ to.mozSrcObject = from.mozSrcObject;
+ to.play();
+ return to;
+ };
+
+ MediaStreamTrack.getSources = MediaStreamTrack.getSources || function (callback) {
+ if (!callback) {
+ throw new TypeError('Failed to execute \'getSources\' on \'MediaStreamTrack\'' +
+ ': 1 argument required, but only 0 present.');
+ }
+ return callback([]);
+ };
+
+ // Fake get{Video,Audio}Tracks
+ if (!MediaStream.prototype.getVideoTracks) {
+ MediaStream.prototype.getVideoTracks = function () {
+ return [];
+ };
+ }
+ if (!MediaStream.prototype.getAudioTracks) {
+ MediaStream.prototype.getAudioTracks = function () {
+ return [];
+ };
+ }
+
+ AdapterJS.maybeThroughWebRTCReady();
+} else if (navigator.webkitGetUserMedia) {
+ webrtcDetectedBrowser = 'chrome';
+ webrtcDetectedType = 'webkit';
+ webrtcDetectedVersion = parseInt(navigator
+ .userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
+ // check if browser is opera 20+
+ var checkIfOpera = navigator.userAgent.match(/\bOPR\/(\d+)/);
+ if (checkIfOpera !== null) {
+ webrtcDetectedBrowser = 'opera';
+ webrtcDetectedVersion = parseInt(checkIfOpera[1], 10);
+ }
+ // check browser datachannel support
+ if ((webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion >= 31) ||
+ (webrtcDetectedBrowser === 'opera' && webrtcDetectedVersion >= 20)) {
+ webrtcDetectedDCSupport = 'SCTP';
+ } else if (webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion < 30 &&
+ webrtcDetectedVersion > 24) {
+ webrtcDetectedDCSupport = 'RTP';
+ } else {
+ webrtcDetectedDCSupport = '';
+ }
+
+ createIceServer = function (url, username, password) {
+ var iceServer = null;
+ var url_parts = url.split(':');
+ if (url_parts[0].indexOf('stun') === 0) {
+ iceServer = { 'url' : url };
+ } else if (url_parts[0].indexOf('turn') === 0) {
+ iceServer = {
+ 'url' : url,
+ 'credential' : password,
+ 'username' : username
+ };
+ }
+ return iceServer;
+ };
+
+ createIceServers = function (urls, username, password) {
+ var iceServers = [];
+ if (webrtcDetectedVersion >= 34) {
+ iceServers = {
+ 'urls' : urls,
+ 'credential' : password,
+ 'username' : username
+ };
+ } else {
+ for (i = 0; i < urls.length; i++) {
+ var iceServer = createIceServer(urls[i], username, password);
+ if (iceServer !== null) {
+ iceServers.push(iceServer);
+ }
+ }
+ }
+ return iceServers;
+ };
+
+ RTCPeerConnection = function (pcConfig, pcConstraints) {
+ if (webrtcDetectedVersion < 34) {
+ AdapterJS.maybeFixConfiguration(pcConfig);
+ }
+ return new webkitRTCPeerConnection(pcConfig, pcConstraints);
+ };
+
+ window.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+ navigator.getUserMedia = window.getUserMedia;
+
+ attachMediaStream = function (element, stream) {
+ if (typeof element.srcObject !== 'undefined') {
+ element.srcObject = stream;
+ } else if (typeof element.mozSrcObject !== 'undefined') {
+ element.mozSrcObject = stream;
+ } else if (typeof element.src !== 'undefined') {
+ element.src = (stream === null ? '' : URL.createObjectURL(stream));
+ } else {
+ console.log('Error attaching stream to element.');
+ }
+ return element;
+ };
+
+ reattachMediaStream = function (to, from) {
+ to.src = from.src;
+ return to;
+ };
+
+ AdapterJS.maybeThroughWebRTCReady();
+} else { // TRY TO USE PLUGIN
+ // IE 9 is not offering an implementation of console.log until you open a console
+ if (typeof console !== 'object' || typeof console.log !== 'function') {
+ /* jshint -W020 */
+ console = {} || console;
+ // Implemented based on console specs from MDN
+ // You may override these functions
+ console.log = function (arg) {};
+ console.info = function (arg) {};
+ console.error = function (arg) {};
+ console.dir = function (arg) {};
+ console.exception = function (arg) {};
+ console.trace = function (arg) {};
+ console.warn = function (arg) {};
+ console.count = function (arg) {};
+ console.debug = function (arg) {};
+ console.count = function (arg) {};
+ console.time = function (arg) {};
+ console.timeEnd = function (arg) {};
+ console.group = function (arg) {};
+ console.groupCollapsed = function (arg) {};
+ console.groupEnd = function (arg) {};
+ /* jshint +W020 */
+ }
+ webrtcDetectedType = 'plugin';
+ webrtcDetectedDCSupport = 'plugin';
+ AdapterJS.parseWebrtcDetectedBrowser();
+ isIE = webrtcDetectedBrowser === 'IE';
+
+ /* jshint -W035 */
+ AdapterJS.WebRTCPlugin.WaitForPluginReady = function() {
+ while (AdapterJS.WebRTCPlugin.pluginState !== AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) {
+ /* empty because it needs to prevent the function from running. */
+ }
+ };
+ /* jshint +W035 */
+
+ AdapterJS.WebRTCPlugin.callWhenPluginReady = function (callback) {
+ if (AdapterJS.WebRTCPlugin.pluginState === AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) {
+ // Call immediately if possible
+ // Once the plugin is set, the code will always take this path
+ callback();
+ } else {
+ // otherwise start a 100ms interval
+ var checkPluginReadyState = setInterval(function () {
+ if (AdapterJS.WebRTCPlugin.pluginState === AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) {
+ clearInterval(checkPluginReadyState);
+ callback();
+ }
+ }, 100);
+ }
+ };
+
+ AdapterJS.WebRTCPlugin.setLogLevel = function(logLevel) {
+ AdapterJS.WebRTCPlugin.callWhenPluginReady(function() {
+ AdapterJS.WebRTCPlugin.plugin.setLogLevel(logLevel);
+ });
+ };
+
+ AdapterJS.WebRTCPlugin.injectPlugin = function () {
+ // only inject once the page is ready
+ if (document.readyState !== 'complete') {
+ return;
+ }
+
+ // Prevent multiple injections
+ if (AdapterJS.WebRTCPlugin.pluginState !== AdapterJS.WebRTCPlugin.PLUGIN_STATES.INITIALIZING) {
+ return;
+ }
+
+ AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INJECTING;
+
+ if (webrtcDetectedBrowser === 'IE' && webrtcDetectedVersion <= 10) {
+ var frag = document.createDocumentFragment();
+ AdapterJS.WebRTCPlugin.plugin = document.createElement('div');
+ AdapterJS.WebRTCPlugin.plugin.innerHTML = '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ // uncomment to be able to use virtual cams
+ (AdapterJS.options.getAllCams ? ' ':'') +
+
+ ' ';
+ while (AdapterJS.WebRTCPlugin.plugin.firstChild) {
+ frag.appendChild(AdapterJS.WebRTCPlugin.plugin.firstChild);
+ }
+ document.body.appendChild(frag);
+
+ // Need to re-fetch the plugin
+ AdapterJS.WebRTCPlugin.plugin =
+ document.getElementById(AdapterJS.WebRTCPlugin.pluginInfo.pluginId);
+ } else {
+ // Load Plugin
+ AdapterJS.WebRTCPlugin.plugin = document.createElement('object');
+ AdapterJS.WebRTCPlugin.plugin.id =
+ AdapterJS.WebRTCPlugin.pluginInfo.pluginId;
+ // IE will only start the plugin if it's ACTUALLY visible
+ if (isIE) {
+ AdapterJS.WebRTCPlugin.plugin.width = '1px';
+ AdapterJS.WebRTCPlugin.plugin.height = '1px';
+ } else { // The size of the plugin on Safari should be 0x0px
+ // so that the autorisation prompt is at the top
+ AdapterJS.WebRTCPlugin.plugin.width = '0px';
+ AdapterJS.WebRTCPlugin.plugin.height = '0px';
+ }
+ AdapterJS.WebRTCPlugin.plugin.type = AdapterJS.WebRTCPlugin.pluginInfo.type;
+ AdapterJS.WebRTCPlugin.plugin.innerHTML = ' ' +
+ ' ' +
+ ' ' +
+ (AdapterJS.options.getAllCams ? ' ':'') +
+ ' ' +
+ ' ';
+ document.body.appendChild(AdapterJS.WebRTCPlugin.plugin);
+ }
+
+
+ AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INJECTED;
+ };
+
+ AdapterJS.WebRTCPlugin.isPluginInstalled =
+ function (comName, plugName, installedCb, notInstalledCb) {
+ if (!isIE) {
+ var pluginArray = navigator.plugins;
+ for (var i = 0; i < pluginArray.length; i++) {
+ if (pluginArray[i].name.indexOf(plugName) >= 0) {
+ installedCb();
+ return;
+ }
+ }
+ notInstalledCb();
+ } else {
+ try {
+ var axo = new ActiveXObject(comName + '.' + plugName);
+ } catch (e) {
+ notInstalledCb();
+ return;
+ }
+ installedCb();
+ }
+ };
+
+ AdapterJS.WebRTCPlugin.defineWebRTCInterface = function () {
+ if (AdapterJS.WebRTCPlugin.pluginState ===
+ AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) {
+ console.error("WebRTC interface has been defined already");
+ return;
+ }
+ AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INITIALIZING;
+
+ AdapterJS.isDefined = function (variable) {
+ return variable !== null && variable !== undefined;
+ };
+
+ createIceServer = function (url, username, password) {
+ var iceServer = null;
+ var url_parts = url.split(':');
+ if (url_parts[0].indexOf('stun') === 0) {
+ iceServer = {
+ 'url' : url,
+ 'hasCredentials' : false
+ };
+ } else if (url_parts[0].indexOf('turn') === 0) {
+ iceServer = {
+ 'url' : url,
+ 'hasCredentials' : true,
+ 'credential' : password,
+ 'username' : username
+ };
+ }
+ return iceServer;
+ };
+
+ createIceServers = function (urls, username, password) {
+ var iceServers = [];
+ for (var i = 0; i < urls.length; ++i) {
+ iceServers.push(createIceServer(urls[i], username, password));
+ }
+ return iceServers;
+ };
+
+ RTCSessionDescription = function (info) {
+ AdapterJS.WebRTCPlugin.WaitForPluginReady();
+ return AdapterJS.WebRTCPlugin.plugin.
+ ConstructSessionDescription(info.type, info.sdp);
+ };
+
+ RTCPeerConnection = function (servers, constraints) {
+ var iceServers = null;
+ if (servers) {
+ iceServers = servers.iceServers;
+ for (var i = 0; i < iceServers.length; i++) {
+ if (iceServers[i].urls && !iceServers[i].url) {
+ iceServers[i].url = iceServers[i].urls;
+ }
+ iceServers[i].hasCredentials = AdapterJS.
+ isDefined(iceServers[i].username) &&
+ AdapterJS.isDefined(iceServers[i].credential);
+ }
+ }
+ var mandatory = (constraints && constraints.mandatory) ?
+ constraints.mandatory : null;
+ var optional = (constraints && constraints.optional) ?
+ constraints.optional : null;
+
+ AdapterJS.WebRTCPlugin.WaitForPluginReady();
+ return AdapterJS.WebRTCPlugin.plugin.
+ PeerConnection(AdapterJS.WebRTCPlugin.pageId,
+ iceServers, mandatory, optional);
+ };
+
+ MediaStreamTrack = {};
+ MediaStreamTrack.getSources = function (callback) {
+ AdapterJS.WebRTCPlugin.callWhenPluginReady(function() {
+ AdapterJS.WebRTCPlugin.plugin.GetSources(callback);
+ });
+ };
+
+ window.getUserMedia = function (constraints, successCallback, failureCallback) {
+ constraints.audio = constraints.audio || false;
+ constraints.video = constraints.video || false;
+
+ AdapterJS.WebRTCPlugin.callWhenPluginReady(function() {
+ AdapterJS.WebRTCPlugin.plugin.
+ getUserMedia(constraints, successCallback, failureCallback);
+ });
+ };
+ window.navigator.getUserMedia = window.getUserMedia;
+
+ attachMediaStream = function (element, stream) {
+ if (!element || !element.parentNode) {
+ return;
+ }
+
+ var streamId
+ if (stream === null) {
+ streamId = '';
+ }
+ else {
+ stream.enableSoundTracks(true); // TODO: remove on 0.12.0
+ streamId = stream.id;
+ }
+
+ var elementId = element.id.length === 0 ? Math.random().toString(36).slice(2) : element.id;
+ if (!element.isWebRTCPlugin || !element.isWebRTCPlugin()) {
+ var tag;
+ switch(element.nodeName.toLowerCase()) {
+ case 'audio':
+ tag = AdapterJS.WebRTCPlugin.TAGS.AUDIO;
+ break;
+ case 'video':
+ tag = AdapterJS.WebRTCPlugin.TAGS.VIDEO;
+ break;
+ default:
+ tag = AdapterJS.WebRTCPlugin.TAGS.NONE;
+ }
+
+ var frag = document.createDocumentFragment();
+ var temp = document.createElement('div');
+ var classHTML = '';
+ if (element.className) {
+ classHTML = 'class="' + element.className + '" ';
+ } else if (element.attributes && element.attributes['class']) {
+ classHTML = 'class="' + element.attributes['class'].value + '" ';
+ }
+
+ temp.innerHTML = '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ';
+ while (temp.firstChild) {
+ frag.appendChild(temp.firstChild);
+ }
+
+ var height = '';
+ var width = '';
+ if (element.getBoundingClientRect) {
+ var rectObject = element.getBoundingClientRect();
+ width = rectObject.width + 'px';
+ height = rectObject.height + 'px';
+ }
+ else if (element.width) {
+ width = element.width;
+ height = element.height;
+ } else {
+ // TODO: What scenario could bring us here?
+ }
+
+ element.parentNode.insertBefore(frag, element);
+ frag = document.getElementById(elementId);
+ frag.width = width;
+ frag.height = height;
+ element.parentNode.removeChild(element);
+ } else {
+ var children = element.children;
+ for (var i = 0; i !== children.length; ++i) {
+ if (children[i].name === 'streamId') {
+ children[i].value = streamId;
+ break;
+ }
+ }
+ element.setStreamId(streamId);
+ }
+ var newElement = document.getElementById(elementId);
+ newElement.onplaying = (element.onplaying) ? element.onplaying : function (arg) {};
+ if (isIE) { // on IE the event needs to be plugged manually
+ newElement.attachEvent('onplaying', newElement.onplaying);
+ newElement.onclick = (element.onclick) ? element.onclick : function (arg) {};
+ newElement._TemOnClick = function (id) {
+ var arg = {
+ srcElement : document.getElementById(id)
+ };
+ newElement.onclick(arg);
+ };
+ }
+
+ return newElement;
+ };
+
+ reattachMediaStream = function (to, from) {
+ var stream = null;
+ var children = from.children;
+ for (var i = 0; i !== children.length; ++i) {
+ if (children[i].name === 'streamId') {
+ AdapterJS.WebRTCPlugin.WaitForPluginReady();
+ stream = AdapterJS.WebRTCPlugin.plugin
+ .getStreamWithId(AdapterJS.WebRTCPlugin.pageId, children[i].value);
+ break;
+ }
+ }
+ if (stream !== null) {
+ return attachMediaStream(to, stream);
+ } else {
+ console.log('Could not find the stream associated with this element');
+ }
+ };
+
+ RTCIceCandidate = function (candidate) {
+ if (!candidate.sdpMid) {
+ candidate.sdpMid = '';
+ }
+
+ AdapterJS.WebRTCPlugin.WaitForPluginReady();
+ return AdapterJS.WebRTCPlugin.plugin.ConstructIceCandidate(
+ candidate.sdpMid, candidate.sdpMLineIndex, candidate.candidate
+ );
+ };
+
+ // inject plugin
+ AdapterJS.addEvent(document, 'readystatechange', AdapterJS.WebRTCPlugin.injectPlugin);
+ AdapterJS.WebRTCPlugin.injectPlugin();
+ };
+
+ // This function will be called if the plugin is needed (browser different
+ // from Chrome or Firefox), but the plugin is not installed.
+ AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb = AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb ||
+ function() {
+ AdapterJS.addEvent(document,
+ 'readystatechange',
+ AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv);
+ AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv();
+ };
+
+ AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv = function () {
+ if (AdapterJS.options.hidePluginInstallPrompt) {
+ return;
+ }
+
+ var downloadLink = AdapterJS.WebRTCPlugin.pluginInfo.downloadLink;
+ if(downloadLink) { // if download link
+ var popupString;
+ if (AdapterJS.WebRTCPlugin.pluginInfo.portalLink) { // is portal link
+ popupString = 'This website requires you to install the ' +
+ ' ' + AdapterJS.WebRTCPlugin.pluginInfo.companyName +
+ ' WebRTC Plugin ' +
+ ' to work on this browser.';
+ } else { // no portal link, just print a generic explanation
+ popupString = AdapterJS.TEXT.PLUGIN.REQUIRE_INSTALLATION;
+ }
+
+ AdapterJS.renderNotificationBar(popupString, AdapterJS.TEXT.PLUGIN.BUTTON, downloadLink);
+ } else { // no download link, just print a generic explanation
+ AdapterJS.renderNotificationBar(AdapterJS.TEXT.PLUGIN.NOT_SUPPORTED);
+ }
+ };
+
+ // Try to detect the plugin and act accordingly
+ AdapterJS.WebRTCPlugin.isPluginInstalled(
+ AdapterJS.WebRTCPlugin.pluginInfo.prefix,
+ AdapterJS.WebRTCPlugin.pluginInfo.plugName,
+ AdapterJS.WebRTCPlugin.defineWebRTCInterface,
+ AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb);
+}
+
+},{}],11:[function(require,module,exports){
+/* global Strophe, APP, $, config, interfaceConfig, toastr */
+var UI = {};
+
+var VideoLayout = require("./videolayout/VideoLayout.js");
+var AudioLevels = require("./audio_levels/AudioLevels.js");
+var Prezi = require("./prezi/Prezi.js");
+var Etherpad = require("./etherpad/Etherpad.js");
+var Chat = require("./side_pannels/chat/Chat.js");
+var Toolbar = require("./toolbars/Toolbar");
+var ToolbarToggler = require("./toolbars/ToolbarToggler");
+var BottomToolbar = require("./toolbars/BottomToolbar");
+var ContactList = require("./side_pannels/contactlist/ContactList");
+var Avatar = require("./avatar/Avatar");
+var EventEmitter = require("events");
+var SettingsMenu = require("./side_pannels/settings/SettingsMenu");
+var Settings = require("./../settings/Settings");
+var PanelToggler = require("./side_pannels/SidePanelToggler");
+var RoomNameGenerator = require("./welcome_page/RoomnameGenerator");
+UI.messageHandler = require("./util/MessageHandler");
+var messageHandler = UI.messageHandler;
+var Authentication = require("./authentication/Authentication");
+var UIUtil = require("./util/UIUtil");
+var NicknameHandler = require("./util/NicknameHandler");
+var JitsiPopover = require("./util/JitsiPopover");
+var CQEvents = require("../../service/connectionquality/CQEvents");
+var DesktopSharingEventTypes
+ = require("../../service/desktopsharing/DesktopSharingEventTypes");
+var RTCEvents = require("../../service/RTC/RTCEvents");
+var RTCBrowserType = require("../RTC/RTCBrowserType");
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var UIEvents = require("../../service/UI/UIEvents");
+var MemberEvents = require("../../service/members/Events");
+
+var eventEmitter = new EventEmitter();
+var roomName = null;
+
+
+function promptDisplayName() {
+ var message = '';
+ message += APP.translation.translateString(
+ "dialog.displayNameRequired");
+ message += ' ' +
+ ' ';
+
+ var buttonTxt
+ = APP.translation.generateTranslationHTML("dialog.Ok");
+ var buttons = [];
+ buttons.push({title: buttonTxt, value: "ok"});
+
+ messageHandler.openDialog(null, message,
+ true,
+ buttons,
+ function (e, v, m, f) {
+ if (v == "ok") {
+ var displayName = f.displayName;
+ if (displayName) {
+ VideoLayout.inputDisplayNameHandler(displayName);
+ return true;
+ }
+ }
+ e.preventDefault();
+ },
+ function () {
+ var form = $.prompt.getPrompt();
+ var input = form.find("input[name='displayName']");
+ input.focus();
+ var button = form.find("button");
+ button.attr("disabled", "disabled");
+ input.keyup(function () {
+ if(!input.val())
+ button.attr("disabled", "disabled");
+ else
+ button.removeAttr("disabled");
+ });
+ }
+ );
+}
+
+function notifyForInitialMute() {
+ messageHandler.notify(null, "notify.mutedTitle", "connected",
+ "notify.muted", null, {timeOut: 120000});
+}
+
+function setupPrezi() {
+ $("#reloadPresentationLink").click(function() {
+ Prezi.reloadPresentation();
+ });
+}
+
+function setupChat() {
+ Chat.init();
+ $("#toggle_smileys").click(function() {
+ Chat.toggleSmileys();
+ });
+}
+
+function setupToolbars() {
+ Toolbar.init(UI);
+ Toolbar.setupButtonsFromConfig();
+ BottomToolbar.init();
+}
+
+function streamHandler(stream, isMuted) {
+ switch (stream.type) {
+ case "audio":
+ VideoLayout.changeLocalAudio(stream, isMuted);
+ break;
+ case "video":
+ VideoLayout.changeLocalVideo(stream, isMuted);
+ break;
+ case "stream":
+ VideoLayout.changeLocalStream(stream, isMuted);
+ break;
+ }
+}
+
+function onXmppConnectionFailed(stropheErrorMsg) {
+
+ var title = APP.translation.generateTranslationHTML(
+ "dialog.error");
+
+ var message;
+ if (stropheErrorMsg) {
+ message = APP.translation.generateTranslationHTML(
+ "dialog.connectErrorWithMsg", {msg: stropheErrorMsg});
+ } else {
+ message = APP.translation.generateTranslationHTML(
+ "dialog.connectError");
+ }
+
+ messageHandler.openDialog(
+ title, message, true, {}, function (e, v, m, f) { return false; });
+}
+
+function onDisposeConference(unload) {
+ Toolbar.showAuthenticateButton(false);
+}
+
+function onDisplayNameChanged(jid, displayName) {
+ ContactList.onDisplayNameChange(jid, displayName);
+ SettingsMenu.onDisplayNameChange(jid, displayName);
+ VideoLayout.onDisplayNameChanged(jid, displayName);
+}
+
+function registerListeners() {
+ APP.RTC.addStreamListener(streamHandler,
+ StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
+ APP.RTC.addStreamListener(streamHandler,
+ StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED);
+ APP.RTC.addStreamListener(function (stream) {
+ VideoLayout.onRemoteStreamAdded(stream);
+ }, StreamEventTypes.EVENT_TYPE_REMOTE_CREATED);
+ APP.RTC.addListener(RTCEvents.LASTN_CHANGED, onLastNChanged);
+ APP.RTC.addListener(RTCEvents.DOMINANTSPEAKER_CHANGED,
+ function (resourceJid) {
+ VideoLayout.onDominantSpeakerChanged(resourceJid);
+ });
+ APP.RTC.addListener(RTCEvents.LASTN_ENDPOINT_CHANGED,
+ function (lastNEndpoints, endpointsEnteringLastN, stream) {
+ VideoLayout.onLastNEndpointsChanged(lastNEndpoints,
+ endpointsEnteringLastN, stream);
+ });
+ APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED,
+ function (devices) {
+ VideoLayout.setDeviceAvailabilityIcons(null, devices);
+ });
+ APP.RTC.addListener(RTCEvents.VIDEO_MUTE, UI.setVideoMuteButtonsState);
+ APP.RTC.addListener(RTCEvents.DATA_CHANNEL_OPEN, function () {
+ // when the data channel becomes available, tell the bridge about video
+ // selections so that it can do adaptive simulcast,
+ // we want the notification to trigger even if userJid is undefined,
+ // or null.
+ var userResource = APP.UI.getLargeVideoResource();
+ eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, userResource);
+ });
+ APP.statistics.addAudioLevelListener(function(jid, audioLevel) {
+ var resourceJid;
+ if(jid === APP.statistics.LOCAL_JID) {
+ resourceJid = AudioLevels.LOCAL_LEVEL;
+ if(APP.RTC.localAudio.isMuted()) {
+ audioLevel = 0;
+ }
+ } else {
+ resourceJid = Strophe.getResourceFromJid(jid);
+ }
+
+ AudioLevels.updateAudioLevel(resourceJid, audioLevel,
+ UI.getLargeVideoResource());
+ });
+ APP.desktopsharing.addListener(function () {
+ ToolbarToggler.showDesktopSharingButton();
+ }, DesktopSharingEventTypes.INIT);
+ APP.desktopsharing.addListener(
+ Toolbar.changeDesktopSharingButtonState,
+ DesktopSharingEventTypes.SWITCHING_DONE);
+ APP.connectionquality.addListener(CQEvents.LOCALSTATS_UPDATED,
+ VideoLayout.updateLocalConnectionStats);
+ APP.connectionquality.addListener(CQEvents.REMOTESTATS_UPDATED,
+ VideoLayout.updateConnectionStats);
+ APP.connectionquality.addListener(CQEvents.STOP,
+ VideoLayout.onStatsStop);
+ APP.xmpp.addListener(XMPPEvents.CONNECTION_FAILED, onXmppConnectionFailed);
+ APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference);
+ APP.xmpp.addListener(XMPPEvents.GRACEFUL_SHUTDOWN, function () {
+ messageHandler.openMessageDialog(
+ 'dialog.serviceUnavailable',
+ 'dialog.gracefulShutdown'
+ );
+ });
+ APP.xmpp.addListener(XMPPEvents.RESERVATION_ERROR, function (code, msg) {
+ var title = APP.translation.generateTranslationHTML(
+ "dialog.reservationError");
+ var message = APP.translation.generateTranslationHTML(
+ "dialog.reservationErrorMsg", {code: code, msg: msg});
+ messageHandler.openDialog(
+ title,
+ message,
+ true, {},
+ function (event, value, message, formVals) {
+ return false;
+ }
+ );
+ });
+ APP.xmpp.addListener(XMPPEvents.KICKED, function () {
+ messageHandler.openMessageDialog("dialog.sessTerminated",
+ "dialog.kickMessage");
+ });
+ APP.xmpp.addListener(XMPPEvents.MUC_DESTROYED, function (reason) {
+ //FIXME: use Session Terminated from translation, but
+ // 'reason' text comes from XMPP packet and is not translated
+ var title = APP.translation.generateTranslationHTML("dialog.sessTerminated");
+ messageHandler.openDialog(
+ title, reason, true, {},
+ function (event, value, message, formVals) {
+ return false;
+ }
+ );
+ });
+ APP.xmpp.addListener(XMPPEvents.BRIDGE_DOWN, function () {
+ messageHandler.showError("dialog.error",
+ "dialog.bridgeUnavailable");
+ });
+ APP.xmpp.addListener(XMPPEvents.USER_ID_CHANGED, function (from, id) {
+ Avatar.setUserAvatar(from, id);
+ });
+ APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, onDisplayNameChanged);
+ APP.xmpp.addListener(XMPPEvents.MUC_JOINED, onMucJoined);
+ APP.xmpp.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, onLocalRoleChanged);
+ APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, onMucMemberJoined);
+ APP.xmpp.addListener(XMPPEvents.MUC_ROLE_CHANGED, onMucRoleChanged);
+ APP.xmpp.addListener(XMPPEvents.PRESENCE_STATUS, onMucPresenceStatus);
+ APP.xmpp.addListener(XMPPEvents.SUBJECT_CHANGED, chatSetSubject);
+ APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft);
+ APP.xmpp.addListener(XMPPEvents.PASSWORD_REQUIRED, onPasswordRequired);
+ APP.xmpp.addListener(XMPPEvents.ETHERPAD, initEtherpad);
+ APP.xmpp.addListener(XMPPEvents.AUTHENTICATION_REQUIRED,
+ onAuthenticationRequired);
+ APP.xmpp.addListener(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
+ onPeerVideoTypeChanged);
+ APP.xmpp.addListener(XMPPEvents.DEVICE_AVAILABLE,
+ function (resource, devices) {
+ VideoLayout.setDeviceAvailabilityIcons(resource, devices);
+ });
+
+ APP.xmpp.addListener(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
+ VideoLayout.onAudioMute);
+ APP.xmpp.addListener(XMPPEvents.PARTICIPANT_VIDEO_MUTED,
+ VideoLayout.onVideoMute);
+ APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS, function (doMuteAudio) {
+ UI.setAudioMuted(doMuteAudio);
+ });
+ APP.members.addListener(MemberEvents.DTMF_SUPPORT_CHANGED,
+ onDtmfSupportChanged);
+ APP.xmpp.addListener(XMPPEvents.START_MUTED_SETTING_CHANGED, function (audio, video) {
+ SettingsMenu.setStartMuted(audio, video);
+ });
+ APP.xmpp.addListener(XMPPEvents.START_MUTED_FROM_FOCUS, function (audio, video) {
+ UI.setInitialMuteFromFocus(audio, video);
+ });
+
+ APP.xmpp.addListener(XMPPEvents.JINGLE_FATAL_ERROR, function (session, error) {
+ UI.messageHandler.showError("dialog.sorry",
+ "dialog.internalError");
+ });
+
+ APP.xmpp.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR, function () {
+ messageHandler.showError("dialog.error",
+ "dialog.SLDFailure");
+ });
+ APP.xmpp.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR, function () {
+ messageHandler.showError("dialog.error",
+ "dialog.SRDFailure");
+ });
+ APP.xmpp.addListener(XMPPEvents.CREATE_ANSWER_ERROR, function () {
+ messageHandler.showError();
+ });
+ APP.xmpp.addListener(XMPPEvents.PROMPT_FOR_LOGIN, function () {
+ // FIXME: re-use LoginDialog which supports retries
+ UI.showLoginPopup(connect);
+ });
+
+ APP.xmpp.addListener(XMPPEvents.FOCUS_DISCONNECTED, function (focusComponent, retrySec) {
+ UI.messageHandler.notify(
+ null, "notify.focus",
+ 'disconnected', "notify.focusFail",
+ {component: focusComponent, ms: retrySec});
+ });
+
+ APP.xmpp.addListener(XMPPEvents.ROOM_JOIN_ERROR, function (pres) {
+ UI.messageHandler.openReportDialog(null,
+ "dialog.joinError", pres);
+ });
+ APP.xmpp.addListener(XMPPEvents.ROOM_CONNECT_ERROR, function (pres) {
+ UI.messageHandler.openReportDialog(null,
+ "dialog.connectError", pres);
+ });
+
+ APP.xmpp.addListener(XMPPEvents.READY_TO_JOIN, function () {
+ var roomName = UI.generateRoomName();
+ APP.xmpp.allocateConferenceFocus(roomName, UI.checkForNicknameAndJoin);
+ });
+
+ //NicknameHandler emits this event
+ UI.addListener(UIEvents.NICKNAME_CHANGED, function (nickname) {
+ APP.xmpp.addToPresence("displayName", nickname);
+ });
+
+ UI.addListener(UIEvents.LARGEVIDEO_INIT, function () {
+ AudioLevels.init();
+ });
+
+ if (!interfaceConfig.filmStripOnly) {
+ APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, updateChatConversation);
+ APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError);
+ // Listens for video interruption events.
+ APP.xmpp.addListener(XMPPEvents.CONNECTION_INTERRUPTED, VideoLayout.onVideoInterrupted);
+ // Listens for video restores events.
+ APP.xmpp.addListener(XMPPEvents.CONNECTION_RESTORED, VideoLayout.onVideoRestored);
+ }
+}
+
+
+/**
+ * Mutes/unmutes the local video.
+ *
+ * @param mute true to mute the local video; otherwise, false
+ * @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 taken by the application logic)
+ */
+function setVideoMute(mute, options) {
+ APP.RTC.setVideoMute(mute,
+ UI.setVideoMuteButtonsState,
+ options);
+}
+
+function onResize() {
+ Chat.resizeChat();
+ VideoLayout.resizeLargeVideoContainer();
+}
+
+function bindEvents() {
+ /**
+ * Resizes and repositions videos in full screen mode.
+ */
+ $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
+ onResize);
+
+ $(window).resize(onResize);
+}
+
+UI.start = function (init) {
+ document.title = interfaceConfig.APP_NAME;
+ var setupWelcomePage = null;
+ if(config.enableWelcomePage && window.location.pathname == "/" &&
+ (!window.localStorage.welcomePageDisabled ||
+ window.localStorage.welcomePageDisabled == "false")) {
+ $("#videoconference_page").hide();
+ if (!setupWelcomePage)
+ setupWelcomePage = require("./welcome_page/WelcomePage");
+ setupWelcomePage();
+
+ return;
+ }
+
+ $("#welcome_page").hide();
+
+ // Set the defaults for prompt dialogs.
+ $.prompt.setDefaults({persistent: false});
+
+
+ registerListeners();
+
+ VideoLayout.init(eventEmitter);
+ NicknameHandler.init(eventEmitter);
+
+ bindEvents();
+ setupPrezi();
+ if (!interfaceConfig.filmStripOnly) {
+ $("#videospace").mousemove(function () {
+ return ToolbarToggler.showToolbar();
+ });
+ setupToolbars();
+ setupChat();
+ // Display notice message at the top of the toolbar
+ if (config.noticeMessage) {
+ $('#noticeText').text(config.noticeMessage);
+ $('#notice').css({display: 'block'});
+ }
+ $("#downloadlog").click(function (event) {
+ dump(event.target);
+ });
+ }
+ else
+ {
+ $("#header").css("display", "none");
+ $("#bottomToolbar").css("display", "none");
+ $("#downloadlog").css("display", "none");
+ $("#remoteVideos").css("padding", "0px 0px 18px 0px");
+ $("#remoteVideos").css("right", "0px");
+ messageHandler.disableNotifications();
+ $('body').popover("disable");
+// $("[data-toggle=popover]").popover("disable");
+ JitsiPopover.enabled = false;
+ }
+
+ document.title = interfaceConfig.APP_NAME;
+
+
+
+
+
+ if(config.requireDisplayName) {
+ var currentSettings = Settings.getSettings();
+ if (!currentSettings.displayName) {
+ promptDisplayName();
+ }
+ }
+
+ init();
+
+ if (!interfaceConfig.filmStripOnly) {
+ toastr.options = {
+ "closeButton": true,
+ "debug": false,
+ "positionClass": "notification-bottom-right",
+ "onclick": null,
+ "showDuration": "300",
+ "hideDuration": "1000",
+ "timeOut": "2000",
+ "extendedTimeOut": "1000",
+ "showEasing": "swing",
+ "hideEasing": "linear",
+ "showMethod": "fadeIn",
+ "hideMethod": "fadeOut",
+ "reposition": function () {
+ if (PanelToggler.isVisible()) {
+ $("#toast-container").addClass("notification-bottom-right-center");
+ } else {
+ $("#toast-container").removeClass("notification-bottom-right-center");
+ }
+ },
+ "newestOnTop": false
+ };
+
+
+ SettingsMenu.init();
+ }
+
+};
+
+function chatAddError(errorMessage, originalText) {
+ return Chat.chatAddError(errorMessage, originalText);
+}
+
+function chatSetSubject(text) {
+ return Chat.chatSetSubject(text);
+}
+
+function updateChatConversation(from, displayName, message, myjid, stamp) {
+ return Chat.updateChatConversation(from, displayName, message, myjid, stamp);
+}
+
+function onMucJoined(jid, info) {
+ Toolbar.updateRoomUrl(window.location.href);
+ var meHTML = APP.translation.generateTranslationHTML("me");
+ $("#localNick").html(Strophe.getResourceFromJid(jid) + " (" + meHTML + ")");
+
+ var settings = Settings.getSettings();
+
+ // Make sure we configure our avatar id, before creating avatar for us
+ Avatar.setUserAvatar(jid, settings.email || settings.uid);
+
+ // Add myself to the contact list.
+ ContactList.addContact(jid);
+
+ // Once we've joined the muc show the toolbar
+ ToolbarToggler.showToolbar();
+
+ var displayName =
+ config.displayJids ? Strophe.getResourceFromJid(jid) : info.displayName;
+
+ if (displayName)
+ onDisplayNameChanged('localVideoContainer', displayName);
+
+
+ VideoLayout.mucJoined();
+}
+
+function initEtherpad(name) {
+ Etherpad.init(name);
+}
+
+function onMucMemberLeft(jid) {
+ console.log('left.muc', jid);
+ var displayName = $('#participant_' + Strophe.getResourceFromJid(jid) +
+ '>.displayname').html();
+ messageHandler.notify(displayName,'notify.somebody',
+ 'disconnected',
+ 'notify.disconnected');
+ if (!config.startAudioMuted ||
+ config.startAudioMuted > APP.members.size()) {
+ UIUtil.playSoundNotification('userLeft');
+ }
+
+ ContactList.removeContact(jid);
+
+ VideoLayout.participantLeft(jid);
+}
+
+function onLocalRoleChanged(jid, info, pres, isModerator) {
+ console.info("My role changed, new role: " + info.role);
+ onModeratorStatusChanged(isModerator);
+ VideoLayout.showModeratorIndicator();
+ SettingsMenu.onRoleChanged();
+
+ if (isModerator) {
+ Authentication.closeAuthenticationWindow();
+ messageHandler.notify(null, "notify.me",
+ 'connected', "notify.moderator");
+
+ Toolbar.checkAutoRecord();
+ }
+}
+
+function onModeratorStatusChanged(isModerator) {
+ Toolbar.showSipCallButton(isModerator);
+ Toolbar.showRecordingButton(
+ isModerator); //&&
+ // FIXME:
+ // Recording visible if
+ // there are at least 2(+ 1 focus) participants
+ //Object.keys(connection.emuc.members).length >= 3);
+}
+
+function onPasswordRequired(callback) {
+ // password is required
+ Toolbar.lockLockButton();
+ var message = '';
+ message += APP.translation.translateString(
+ "dialog.passwordRequired");
+ message += ' ' +
+ ' ';
+
+ messageHandler.openTwoButtonDialog(null, null, null, message,
+ true,
+ "dialog.Ok",
+ function (e, v, m, f) {},
+ null,
+ function (e, v, m, f) {
+ if (v) {
+ var lockKey = f.lockKey;
+ if (lockKey) {
+ Toolbar.setSharedKey(lockKey);
+ callback(lockKey);
+ }
+ }
+ },
+ ':input:first'
+ );
+}
+
+/**
+ * The dialpad button is shown iff there is at least one member that supports
+ * DTMF (e.g. jigasi).
+ */
+function onDtmfSupportChanged(dtmfSupport) {
+ //TODO: enable when the UI is ready
+ //Toolbar.showDialPadButton(dtmfSupport);
+}
+
+function onMucMemberJoined(jid, id, displayName) {
+ messageHandler.notify(displayName,'notify.somebody',
+ 'connected',
+ 'notify.connected');
+
+ if (!config.startAudioMuted ||
+ config.startAudioMuted > APP.members.size())
+ UIUtil.playSoundNotification('userJoined');
+
+ // Configure avatar
+ Avatar.setUserAvatar(jid, id);
+
+ // Add Peer's container
+ VideoLayout.ensurePeerContainerExists(jid);
+}
+
+function onMucPresenceStatus(jid, info) {
+ VideoLayout.setPresenceStatus(Strophe.getResourceFromJid(jid), info.status);
+}
+
+function onPeerVideoTypeChanged(resourceJid, newVideoType) {
+ VideoLayout.onVideoTypeChanged(resourceJid, newVideoType);
+}
+
+function onMucRoleChanged(role, displayName) {
+ VideoLayout.showModeratorIndicator();
+
+ if (role === 'moderator') {
+ var messageKey, messageOptions = {};
+ if (!displayName) {
+ messageKey = "notify.grantedToUnknown";
+ }
+ else {
+ messageKey = "notify.grantedTo";
+ messageOptions = {to: displayName};
+ }
+ messageHandler.notify(
+ displayName,'notify.somebody',
+ 'connected', messageKey,
+ messageOptions);
+ }
+}
+
+function onAuthenticationRequired(intervalCallback) {
+ Authentication.openAuthenticationDialog(
+ roomName, intervalCallback, function () {
+ Toolbar.authenticateClicked();
+ });
+}
+
+
+function onLastNChanged(oldValue, newValue) {
+ if (config.muteLocalVideoIfNotInLastN) {
+ setVideoMute(!newValue, { 'byUser': false });
+ }
+}
+
+
+UI.toggleSmileys = function () {
+ Chat.toggleSmileys();
+};
+
+UI.getSettings = function () {
+ return Settings.getSettings();
+};
+
+UI.toggleFilmStrip = function () {
+ return BottomToolbar.toggleFilmStrip();
+};
+
+UI.toggleChat = function () {
+ return BottomToolbar.toggleChat();
+};
+
+UI.toggleContactList = function () {
+ return BottomToolbar.toggleContactList();
+};
+
+UI.inputDisplayNameHandler = function (value) {
+ VideoLayout.inputDisplayNameHandler(value);
+};
+
+UI.getLargeVideoResource = function () {
+ return VideoLayout.getLargeVideoResource();
+};
+
+UI.generateRoomName = function() {
+ if(roomName)
+ return roomName;
+ var roomnode = null;
+ var path = window.location.pathname;
+
+ // determinde the room node from the url
+ // TODO: just the roomnode or the whole bare jid?
+ if (config.getroomnode && typeof config.getroomnode === 'function') {
+ // custom function might be responsible for doing the pushstate
+ roomnode = config.getroomnode(path);
+ } else {
+ /* fall back to default strategy
+ * this is making assumptions about how the URL->room mapping happens.
+ * It currently assumes deployment at root, with a rewrite like the
+ * following one (for nginx):
+ location ~ ^/([a-zA-Z0-9]+)$ {
+ rewrite ^/(.*)$ / break;
+ }
+ */
+ if (path.length > 1) {
+ roomnode = path.substr(1).toLowerCase();
+ } else {
+ var word = RoomNameGenerator.generateRoomWithoutSeparator();
+ roomnode = word.toLowerCase();
+
+ window.history.pushState('VideoChat',
+ 'Room: ' + word, window.location.pathname + word);
+ }
+ }
+
+ roomName = roomnode + '@' + config.hosts.muc;
+ return roomName;
+};
+
+
+UI.connectionIndicatorShowMore = function(jid) {
+ return VideoLayout.showMore(jid);
+};
+
+UI.showLoginPopup = function(callback) {
+ console.log('password is required');
+ var message = '';
+ message += APP.translation.translateString(
+ "dialog.passwordRequired");
+ message += ' ' +
+ ' ' +
+ ' ';
+ UI.messageHandler.openTwoButtonDialog(null, null, null, message,
+ true,
+ "dialog.Ok",
+ function (e, v, m, f) {
+ if (v) {
+ if (f.username !== null && f.password != null) {
+ callback(f.username, f.password);
+ }
+ }
+ },
+ null, null, ':input:first'
+
+ );
+};
+
+UI.checkForNicknameAndJoin = function () {
+
+ Authentication.closeAuthenticationDialog();
+ Authentication.stopInterval();
+
+ var nick = null;
+ if (config.useNicks) {
+ nick = window.prompt('Your nickname (optional)');
+ }
+ APP.xmpp.joinRoom(roomName, config.useNicks, nick);
+};
+
+
+function dump(elem, filename) {
+ elem = elem.parentNode;
+ elem.download = filename || 'meetlog.json';
+ elem.href = 'data:application/json;charset=utf-8,\n';
+ var data = APP.xmpp.getJingleLog();
+ var metadata = {};
+ metadata.time = new Date();
+ metadata.url = window.location.href;
+ metadata.ua = navigator.userAgent;
+ var log = APP.xmpp.getXmppLog();
+ if (log) {
+ metadata.xmpp = log;
+ }
+ data.metadata = metadata;
+ elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
+ return false;
+}
+
+UI.getRoomName = function () {
+ return roomName;
+};
+
+UI.setInitialMuteFromFocus = function (muteAudio, muteVideo) {
+ if (muteAudio || muteVideo)
+ notifyForInitialMute();
+ if (muteAudio)
+ UI.setAudioMuted(true);
+ if (muteVideo)
+ UI.setVideoMute(true);
+};
+
+/**
+ * Mutes/unmutes the local video.
+ */
+UI.toggleVideo = function () {
+ setVideoMute(!APP.RTC.localVideo.isMuted());
+};
+
+/**
+ * Mutes / unmutes audio for the local participant.
+ */
+UI.toggleAudio = function() {
+ UI.setAudioMuted(!APP.RTC.localAudio.isMuted());
+};
+
+/**
+ * Sets muted audio state for the local participant.
+ */
+UI.setAudioMuted = function (mute, earlyMute) {
+ var audioMute = null;
+ if (earlyMute)
+ audioMute = function (mute, cb) {
+ return APP.xmpp.sendAudioInfoPresence(mute, cb);
+ };
+ else
+ audioMute = function (mute, cb) {
+ return APP.xmpp.setAudioMute(mute, cb);
+ };
+ if (!audioMute(mute, function () {
+ VideoLayout.showLocalAudioIndicator(mute);
+
+ UIUtil.buttonClick("#toolbar_button_mute", "icon-microphone icon-mic-disabled");
+ })) {
+ // We still click the button.
+ UIUtil.buttonClick("#toolbar_button_mute", "icon-microphone icon-mic-disabled");
+ return;
+ }
+};
+
+UI.addListener = function (type, listener) {
+ eventEmitter.on(type, listener);
+};
+
+UI.clickOnVideo = function (videoNumber) {
+ var remoteVideos = $(".videocontainer:not(#mixedstream)");
+ if (remoteVideos.length > videoNumber) {
+ remoteVideos[videoNumber].click();
+ }
+};
+
+//Used by torture
+UI.showToolbar = function () {
+ return ToolbarToggler.showToolbar();
+};
+
+//Used by torture
+UI.dockToolbar = function (isDock) {
+ return ToolbarToggler.dockToolbar(isDock);
+};
+
+UI.setVideoMuteButtonsState = function (mute) {
+ var video = $('#toolbar_button_camera');
+ var communicativeClass = "icon-camera";
+ var muteClass = "icon-camera icon-camera-disabled";
+
+ if (mute) {
+ video.removeClass(communicativeClass);
+ video.addClass(muteClass);
+ } else {
+ video.removeClass(muteClass);
+ video.addClass(communicativeClass);
+ }
+};
+
+UI.userAvatarChanged = function (resourceJid, thumbUrl, contactListUrl) {
+ VideoLayout.userAvatarChanged(resourceJid, thumbUrl);
+ ContactList.userAvatarChanged(resourceJid, contactListUrl);
+ if(resourceJid === APP.xmpp.myResource())
+ SettingsMenu.changeAvatar(thumbUrl);
+};
+
+UI.setVideoMute = setVideoMute;
+
+module.exports = UI;
+
+
+},{"../../service/RTC/RTCEvents":160,"../../service/RTC/StreamEventTypes":162,"../../service/UI/UIEvents":163,"../../service/connectionquality/CQEvents":165,"../../service/desktopsharing/DesktopSharingEventTypes":166,"../../service/members/Events":167,"../../service/xmpp/XMPPEvents":169,"../RTC/RTCBrowserType":8,"./../settings/Settings":47,"./audio_levels/AudioLevels.js":12,"./authentication/Authentication":14,"./avatar/Avatar":16,"./etherpad/Etherpad.js":17,"./prezi/Prezi.js":18,"./side_pannels/SidePanelToggler":20,"./side_pannels/chat/Chat.js":21,"./side_pannels/contactlist/ContactList":25,"./side_pannels/settings/SettingsMenu":26,"./toolbars/BottomToolbar":27,"./toolbars/Toolbar":28,"./toolbars/ToolbarToggler":29,"./util/JitsiPopover":30,"./util/MessageHandler":31,"./util/NicknameHandler":32,"./util/UIUtil":33,"./videolayout/VideoLayout.js":39,"./welcome_page/RoomnameGenerator":40,"./welcome_page/WelcomePage":41,"events":170}],12:[function(require,module,exports){
+/* global APP, interfaceConfig, $, Strophe */
+var CanvasUtil = require("./CanvasUtils");
+
+var ASDrawContext = null;
+
+function initActiveSpeakerAudioLevels() {
+ var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
+ var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
+
+ // Draw a circle.
+ ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
+
+ // Add a shadow around the circle
+ ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
+ ASDrawContext.shadowOffsetX = 0;
+ ASDrawContext.shadowOffsetY = 0;
+}
+
+/**
+ * The audio Levels plugin.
+ */
+var AudioLevels = (function(my) {
+ var audioLevelCanvasCache = {};
+
+ my.LOCAL_LEVEL = 'local';
+
+ my.init = function () {
+ ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d');
+ initActiveSpeakerAudioLevels();
+ };
+
+ /**
+ * Updates the audio level canvas for the given peerJid. If the canvas
+ * didn't exist we create it.
+ */
+ my.updateAudioLevelCanvas = function (peerJid, VideoLayout) {
+ var resourceJid = null;
+ var videoSpanId = null;
+ if (!peerJid)
+ videoSpanId = 'localVideoContainer';
+ else {
+ resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ videoSpanId = 'participant_' + resourceJid;
+ }
+
+ var videoSpan = document.getElementById(videoSpanId);
+
+ if (!videoSpan) {
+ if (resourceJid)
+ console.error("No video element for jid", resourceJid);
+ else
+ console.error("No video element for local video.");
+
+ return;
+ }
+
+ var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
+
+ var videoSpaceWidth = $('#remoteVideos').width();
+ var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
+ var thumbnailWidth = thumbnailSize[0];
+ var thumbnailHeight = thumbnailSize[1];
+
+ if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
+
+ audioLevelCanvas = document.createElement('canvas');
+ audioLevelCanvas.className = "audiolevel";
+ audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
+ audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
+ resizeAudioLevelCanvas( audioLevelCanvas,
+ thumbnailWidth,
+ thumbnailHeight);
+
+ videoSpan.appendChild(audioLevelCanvas);
+ } else {
+ audioLevelCanvas = audioLevelCanvas.get(0);
+
+ resizeAudioLevelCanvas( audioLevelCanvas,
+ thumbnailWidth,
+ thumbnailHeight);
+ }
+ };
+
+ /**
+ * Updates the audio level UI for the given resourceJid.
+ *
+ * @param resourceJid the resource jid indicating the video element for
+ * which we draw the audio level
+ * @param audioLevel the newAudio level to render
+ */
+ my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) {
+ drawAudioLevelCanvas(resourceJid, audioLevel);
+
+ var videoSpanId = getVideoSpanId(resourceJid);
+
+ var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
+
+ if (!audioLevelCanvas)
+ return;
+
+ var drawContext = audioLevelCanvas.getContext('2d');
+
+ var canvasCache = audioLevelCanvasCache[resourceJid];
+
+ drawContext.clearRect (0, 0,
+ audioLevelCanvas.width, audioLevelCanvas.height);
+ drawContext.drawImage(canvasCache, 0, 0);
+
+ if(resourceJid === AudioLevels.LOCAL_LEVEL) {
+ if(!APP.xmpp.myJid()) {
+ return;
+ }
+ resourceJid = APP.xmpp.myResource();
+ }
+
+ if(resourceJid === largeVideoResourceJid) {
+ window.requestAnimationFrame(function () {
+ AudioLevels.updateActiveSpeakerAudioLevel(audioLevel);
+ });
+ }
+ };
+
+ my.updateActiveSpeakerAudioLevel = function(audioLevel) {
+ if($("#activeSpeaker").css("visibility") == "hidden" || ASDrawContext === null)
+ return;
+
+ ASDrawContext.clearRect(0, 0, 300, 300);
+ if(audioLevel == 0)
+ return;
+
+ ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
+
+
+ // Fill the shape.
+ ASDrawContext.fill();
+ };
+
+ /**
+ * Resizes the given audio level canvas to match the given thumbnail size.
+ */
+ function resizeAudioLevelCanvas(audioLevelCanvas,
+ thumbnailWidth,
+ thumbnailHeight) {
+ audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
+ audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
+ }
+
+ /**
+ * Draws the audio level canvas into the cached canvas object.
+ *
+ * @param resourceJid the resource jid indicating the video element for
+ * which we draw the audio level
+ * @param audioLevel the newAudio level to render
+ */
+ function drawAudioLevelCanvas(resourceJid, audioLevel) {
+ if (!audioLevelCanvasCache[resourceJid]) {
+
+ var videoSpanId = getVideoSpanId(resourceJid);
+
+ var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
+
+ /*
+ * FIXME Testing has shown that audioLevelCanvasOrig may not exist.
+ * In such a case, the method CanvasUtil.cloneCanvas may throw an
+ * error. Since audio levels are frequently updated, the errors have
+ * been observed to pile into the console, strain the CPU.
+ */
+ if (audioLevelCanvasOrig) {
+ audioLevelCanvasCache[resourceJid] =
+ CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
+ }
+ }
+
+ var canvas = audioLevelCanvasCache[resourceJid];
+
+ if (!canvas)
+ return;
+
+ var drawContext = canvas.getContext('2d');
+
+ drawContext.clearRect(0, 0, canvas.width, canvas.height);
+
+ var shadowLevel = getShadowLevel(audioLevel);
+
+ if (shadowLevel > 0) {
+ // drawContext, x, y, w, h, r, shadowColor, shadowLevel
+ CanvasUtil.drawRoundRectGlow(drawContext,
+ interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
+ canvas.width - interfaceConfig.CANVAS_EXTRA,
+ canvas.height - interfaceConfig.CANVAS_EXTRA,
+ interfaceConfig.CANVAS_RADIUS,
+ interfaceConfig.SHADOW_COLOR,
+ shadowLevel);
+ }
+ }
+
+ /**
+ * Returns the shadow/glow level for the given audio level.
+ *
+ * @param audioLevel the audio level from which we determine the shadow
+ * level
+ */
+ function getShadowLevel (audioLevel) {
+ var shadowLevel = 0;
+
+ if (audioLevel <= 0.3) {
+ shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
+ }
+ else if (audioLevel <= 0.6) {
+ shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
+ }
+ else {
+ shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
+ }
+ return shadowLevel;
+ }
+
+ /**
+ * Returns the video span id corresponding to the given resourceJid or local
+ * user.
+ */
+ function getVideoSpanId(resourceJid) {
+ var videoSpanId = null;
+ if (resourceJid === AudioLevels.LOCAL_LEVEL ||
+ (APP.xmpp.myResource() && resourceJid === APP.xmpp.myResource()))
+ videoSpanId = 'localVideoContainer';
+ else
+ videoSpanId = 'participant_' + resourceJid;
+
+ return videoSpanId;
+ }
+
+ /**
+ * Indicates that the remote video has been resized.
+ */
+ $(document).bind('remotevideo.resized', function (event, width, height) {
+ var resized = false;
+ $('#remoteVideos>span>canvas').each(function() {
+ var canvas = $(this).get(0);
+ if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
+ canvas.width = width + interfaceConfig.CANVAS_EXTRA;
+ resized = true;
+ }
+
+ if (canvas.heigh !== height + interfaceConfig.CANVAS_EXTRA) {
+ canvas.height = height + interfaceConfig.CANVAS_EXTRA;
+ resized = true;
+ }
+ });
+
+ if (resized)
+ Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
+ audioLevelCanvasCache[resourceJid].width =
+ width + interfaceConfig.CANVAS_EXTRA;
+ audioLevelCanvasCache[resourceJid].height =
+ height + interfaceConfig.CANVAS_EXTRA;
+ });
+ });
+
+ return my;
+
+})(AudioLevels || {});
+
+module.exports = AudioLevels;
+},{"./CanvasUtils":13}],13:[function(require,module,exports){
+/**
+ * Utility class for drawing canvas shapes.
+ */
+var CanvasUtil = (function(my) {
+
+ /**
+ * Draws a round rectangle with a glow. The glowWidth indicates the depth
+ * of the glow.
+ *
+ * @param drawContext the context of the canvas to draw to
+ * @param x the x coordinate of the round rectangle
+ * @param y the y coordinate of the round rectangle
+ * @param w the width of the round rectangle
+ * @param h the height of the round rectangle
+ * @param glowColor the color of the glow
+ * @param glowWidth the width of the glow
+ */
+ my.drawRoundRectGlow
+ = function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
+
+ // Save the previous state of the context.
+ drawContext.save();
+
+ if (w < 2 * r) r = w / 2;
+ if (h < 2 * r) r = h / 2;
+
+ // Draw a round rectangle.
+ drawContext.beginPath();
+ drawContext.moveTo(x+r, y);
+ drawContext.arcTo(x+w, y, x+w, y+h, r);
+ drawContext.arcTo(x+w, y+h, x, y+h, r);
+ drawContext.arcTo(x, y+h, x, y, r);
+ drawContext.arcTo(x, y, x+w, y, r);
+ drawContext.closePath();
+
+ // Add a shadow around the rectangle
+ drawContext.shadowColor = glowColor;
+ drawContext.shadowBlur = glowWidth;
+ drawContext.shadowOffsetX = 0;
+ drawContext.shadowOffsetY = 0;
+
+ // Fill the shape.
+ drawContext.fill();
+
+ drawContext.save();
+
+ drawContext.restore();
+
+// 1) Uncomment this line to use Composite Operation, which is doing the
+// same as the clip function below and is also antialiasing the round
+// border, but is said to be less fast performance wise.
+
+// drawContext.globalCompositeOperation='destination-out';
+
+ drawContext.beginPath();
+ drawContext.moveTo(x+r, y);
+ drawContext.arcTo(x+w, y, x+w, y+h, r);
+ drawContext.arcTo(x+w, y+h, x, y+h, r);
+ drawContext.arcTo(x, y+h, x, y, r);
+ drawContext.arcTo(x, y, x+w, y, r);
+ drawContext.closePath();
+
+// 2) Uncomment this line to use Composite Operation, which is doing the
+// same as the clip function below and is also antialiasing the round
+// border, but is said to be less fast performance wise.
+
+// drawContext.fill();
+
+ // Comment these two lines if choosing to do the same with composite
+ // operation above 1 and 2.
+ drawContext.clip();
+ drawContext.clearRect(0, 0, 277, 200);
+
+ // Restore the previous context state.
+ drawContext.restore();
+ };
+
+ /**
+ * Clones the given canvas.
+ *
+ * @return the new cloned canvas.
+ */
+ my.cloneCanvas = function (oldCanvas) {
+ /*
+ * FIXME Testing has shown that oldCanvas may not exist. In such a case,
+ * the method CanvasUtil.cloneCanvas may throw an error. Since audio
+ * levels are frequently updated, the errors have been observed to pile
+ * into the console, strain the CPU.
+ */
+ if (!oldCanvas)
+ return oldCanvas;
+
+ //create a new canvas
+ var newCanvas = document.createElement('canvas');
+ var context = newCanvas.getContext('2d');
+
+ //set dimensions
+ newCanvas.width = oldCanvas.width;
+ newCanvas.height = oldCanvas.height;
+
+ //apply the old canvas to the new one
+ context.drawImage(oldCanvas, 0, 0);
+
+ //return the new canvas
+ return newCanvas;
+ };
+
+ return my;
+})(CanvasUtil || {});
+
+module.exports = CanvasUtil;
+},{}],14:[function(require,module,exports){
+/* global $, APP*/
+
+var LoginDialog = require('./LoginDialog');
+var Moderator = require('../../xmpp/moderator');
+
+/* Initial "authentication required" dialog */
+var authDialog = null;
+/* Loop retry ID that wits for other user to create the room */
+var authRetryId = null;
+var authenticationWindow = null;
+
+var Authentication = {
+ openAuthenticationDialog: function (roomName, intervalCallback, callback) {
+ // This is the loop that will wait for the room to be created by
+ // someone else. 'auth_required.moderator' will bring us back here.
+ authRetryId = window.setTimeout(intervalCallback, 5000);
+ // Show prompt only if it's not open
+ if (authDialog !== null) {
+ return;
+ }
+ // extract room name from 'room@muc.server.net'
+ var room = roomName.substr(0, roomName.indexOf('@'));
+
+ var title
+ = APP.translation.generateTranslationHTML("dialog.WaitingForHost");
+ var msg
+ = APP.translation.generateTranslationHTML(
+ "dialog.WaitForHostMsg", {room: room});
+
+ var buttonTxt
+ = APP.translation.generateTranslationHTML("dialog.IamHost");
+ var buttons = [];
+ buttons.push({title: buttonTxt, value: "authNow"});
+
+ authDialog = APP.UI.messageHandler.openDialog(
+ title,
+ msg,
+ true,
+ buttons,
+ function (onSubmitEvent, submitValue) {
+
+ // Do not close the dialog yet
+ onSubmitEvent.preventDefault();
+
+ // Open login popup
+ if (submitValue === 'authNow') {
+ callback();
+ }
+ }
+ );
+ },
+ closeAuthenticationWindow: function () {
+ if (authenticationWindow) {
+ authenticationWindow.close();
+ authenticationWindow = null;
+ }
+ },
+ xmppAuthenticate: function () {
+
+ var loginDialog = LoginDialog.show(
+ function (connection, state) {
+ if (!state) {
+ // User cancelled
+ loginDialog.close();
+ return;
+ } else if (state == APP.xmpp.Status.CONNECTED) {
+
+ loginDialog.close();
+
+ Authentication.stopInterval();
+ Authentication.closeAuthenticationDialog();
+
+ // Close the connection as anonymous one will be used
+ // to create the conference. Session-id will authorize
+ // the request.
+ connection.disconnect();
+
+ var roomName = APP.UI.generateRoomName();
+ Moderator.allocateConferenceFocus(roomName, function () {
+ // If it's not "on the fly" authentication now join
+ // the conference room
+ if (!APP.xmpp.isMUCJoined()) {
+ APP.UI.checkForNicknameAndJoin();
+ }
+ });
+ }
+ }, true);
+ },
+ focusAuthenticationWindow: function () {
+ // If auth window exists just bring it to the front
+ if (authenticationWindow) {
+ authenticationWindow.focus();
+ return;
+ }
+ },
+ closeAuthenticationDialog: function () {
+ // Close authentication dialog if opened
+ if (authDialog) {
+ authDialog.close();
+ authDialog = null;
+ }
+ },
+ createAuthenticationWindow: function (callback, url) {
+ authenticationWindow = APP.UI.messageHandler.openCenteredPopup(
+ url, 910, 660,
+ // On closed
+ function () {
+ // Close authentication dialog if opened
+ Authentication.closeAuthenticationDialog();
+ callback();
+ authenticationWindow = null;
+ });
+ return authenticationWindow;
+ },
+ stopInterval: function () {
+ // Clear retry interval, so that we don't call 'doJoinAfterFocus' twice
+ if (authRetryId) {
+ window.clearTimeout(authRetryId);
+ authRetryId = null;
+ }
+ }
+};
+
+module.exports = Authentication;
+},{"../../xmpp/moderator":60,"./LoginDialog":15}],15:[function(require,module,exports){
+/* global $, APP, config*/
+
+var XMPP = require('../../xmpp/xmpp');
+var Moderator = require('../../xmpp/moderator');
+
+//FIXME: use LoginDialog to add retries to XMPP.connect method used when
+// anonymous domain is not enabled
+
+/**
+ * Creates new Dialog instance.
+ * @param callback function(Strophe.Connection, Strophe.Status) called
+ * when we either fail to connect or succeed(check Strophe.Status).
+ * @param obtainSession true if we want to send ConferenceIQ to Jicofo
+ * in order to create session-id after the connection is established.
+ * @constructor
+ */
+function Dialog(callback, obtainSession) {
+
+ var self = this;
+
+ var stop = false;
+
+ var connection = APP.xmpp.createConnection();
+
+ var message = '';
+ message += APP.translation.translateString("dialog.passwordRequired");
+ message += ' ' +
+ ' ' +
+ ' ';
+
+ var okButton = APP.translation.generateTranslationHTML("dialog.Ok");
+
+ var cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
+
+ var states = {
+ login: {
+ html: message,
+ buttons: [
+ { title: okButton, value: true},
+ { title: cancelButton, value: false}
+ ],
+ focus: ':input:first',
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if (v) {
+ var jid = f.username;
+ var password = f.password;
+ if (jid && password) {
+ stop = false;
+ connection.reset();
+ connDialog.goToState('connecting');
+ connection.connect(jid, password, stateHandler);
+ }
+ } else {
+ // User cancelled
+ stop = true;
+ callback();
+ }
+ }
+ },
+ connecting: {
+ title: APP.translation.translateString('dialog.connecting'),
+ html: '
',
+ buttons: [],
+ defaultButton: 0
+ },
+ finished: {
+ title: APP.translation.translateString('dialog.error'),
+ html: '
',
+ buttons: [
+ {
+ title: APP.translation.translateString('dialog.retry'),
+ value: 'retry'
+ },
+ {
+ title: APP.translation.translateString('dialog.Cancel'),
+ value: 'cancel'
+ },
+ ],
+ defaultButton: 0,
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if (v === 'retry')
+ connDialog.goToState('login');
+ else
+ callback();
+ }
+ }
+ };
+
+ var connDialog
+ = APP.UI.messageHandler.openDialogWithStates(states,
+ { persistent: true, closeText: '' }, null);
+
+ var stateHandler = function (status, message) {
+ if (stop) {
+ return;
+ }
+
+ var translateKey = "connection." + XMPP.getStatusString(status);
+ var statusStr = APP.translation.translateString(translateKey);
+
+ // Display current state
+ var connectionStatus =
+ connDialog.getState('connecting').find('#connectionStatus');
+
+ connectionStatus.text(statusStr);
+
+ switch (status) {
+ case XMPP.Status.CONNECTED:
+
+ stop = true;
+ if (!obtainSession) {
+ callback(connection, status);
+ return;
+ }
+ // Obtaining session-id status
+ connectionStatus.text(
+ APP.translation.translateString(
+ 'connection.FETCH_SESSION_ID'));
+
+ // Authenticate with Jicofo and obtain session-id
+ var roomName = APP.UI.generateRoomName();
+
+ // Jicofo will return new session-id when connected
+ // from authenticated domain
+ connection.sendIQ(
+ Moderator.createConferenceIq(roomName),
+ function (result) {
+
+ connectionStatus.text(
+ APP.translation.translateString(
+ 'connection.GOT_SESSION_ID'));
+
+ stop = true;
+
+ // Parse session-id
+ Moderator.parseSessionId(result);
+
+ callback(connection, status);
+ },
+ function (error) {
+ console.error("Auth on the fly failed", error);
+
+ stop = true;
+
+ var errorMsg =
+ APP.translation.translateString(
+ 'connection.GET_SESSION_ID_ERROR') +
+ $(error).find('>error').attr('code');
+
+ self.displayError(errorMsg);
+
+ connection.disconnect();
+ });
+
+ break;
+ case XMPP.Status.AUTHFAIL:
+ case XMPP.Status.CONNFAIL:
+ case XMPP.Status.DISCONNECTED:
+
+ stop = true;
+
+ callback(connection, status);
+
+ var errorMessage = statusStr;
+
+ if (message)
+ {
+ errorMessage += ': ' + message;
+ }
+ self.displayError(errorMessage);
+
+ break;
+ default:
+ break;
+ }
+ };
+
+ /**
+ * Displays error message in 'finished' state which allows either to cancel
+ * or retry.
+ * @param message the final message to be displayed.
+ */
+ this.displayError = function (message) {
+
+ var finishedState = connDialog.getState('finished');
+
+ var errorMessageElem = finishedState.find('#errorMessage');
+ errorMessageElem.text(message);
+
+ connDialog.goToState('finished');
+ };
+
+ /**
+ * Closes LoginDialog.
+ */
+ this.close = function () {
+ stop = true;
+ connDialog.close();
+ };
+}
+
+var LoginDialog = {
+
+ /**
+ * Displays login prompt used to establish new XMPP connection. Given
+ * callback(Strophe.Connection, Strophe.Status) function will be
+ * called when we connect successfully(status === CONNECTED) or when we fail
+ * to do so. On connection failure program can call Dialog.close() method in
+ * order to cancel or do nothing to let the user retry.
+ * @param callback function(Strophe.Connection, Strophe.Status)
+ * called when we either fail to connect or succeed(check
+ * Strophe.Status).
+ * @param obtainSession true if we want to send ConferenceIQ to
+ * Jicofo in order to create session-id after the connection is
+ * established.
+ * @returns {Dialog}
+ */
+ show: function (callback, obtainSession) {
+ return new Dialog(callback, obtainSession);
+ }
+};
+
+module.exports = LoginDialog;
+},{"../../xmpp/moderator":60,"../../xmpp/xmpp":68}],16:[function(require,module,exports){
+var Settings = require("../../settings/Settings");
+
+var users = {};
+
+var Avatar = {
+
+ /**
+ * Sets the user's avatar in the settings menu(if local user), contact list
+ * and thumbnail
+ * @param jid jid of the user
+ * @param id email or userID to be used as a hash
+ */
+ setUserAvatar: function (jid, id) {
+ if (id) {
+ if (users[jid] === id) {
+ return;
+ }
+ users[jid] = id;
+ }
+ var thumbUrl = this.getThumbUrl(jid);
+ var contactListUrl = this.getContactListUrl(jid);
+ var resourceJid = Strophe.getResourceFromJid(jid);
+
+ APP.UI.userAvatarChanged(resourceJid, thumbUrl, contactListUrl);
+ },
+ /**
+ * Returns image URL for the avatar to be displayed on large video area
+ * where current active speaker is presented.
+ * @param jid full MUC jid of the user for whom we want to obtain avatar URL
+ */
+ getActiveSpeakerUrl: function (jid) {
+ return this.getGravatarUrl(jid, 100);
+ },
+ /**
+ * Returns image URL for the avatar to be displayed on small video thumbnail
+ * @param jid full MUC jid of the user for whom we want to obtain avatar URL
+ */
+ getThumbUrl: function (jid) {
+ return this.getGravatarUrl(jid, 100);
+ },
+ /**
+ * Returns the URL for the avatar to be displayed as contactlist item
+ * @param jid full MUC jid of the user for whom we want to obtain avatar URL
+ */
+ getContactListUrl: function (jid) {
+ return this.getGravatarUrl(jid, 30);
+ },
+ getGravatarUrl: function (jid, size) {
+ if (!jid) {
+ console.error("Get gravatar - jid is undefined");
+ return null;
+ }
+ var id = users[jid];
+ if (!id) {
+ console.warn(
+ "No avatar stored yet for " + jid + " - using JID as ID");
+ id = jid;
+ }
+ return 'https://www.gravatar.com/avatar/' +
+ MD5.hexdigest(id.trim().toLowerCase()) +
+ "?d=wavatar&size=" + (size || "30");
+ }
+
+};
+
+
+module.exports = Avatar;
+},{"../../settings/Settings":47}],17:[function(require,module,exports){
+/* global $, config,
+ setLargeVideoVisible, Util */
+
+var VideoLayout = require("../videolayout/VideoLayout");
+var Prezi = require("../prezi/Prezi");
+var UIUtil = require("../util/UIUtil");
+
+var etherpadName = null;
+var etherpadIFrame = null;
+var domain = null;
+var options = "?showControls=true&showChat=false&showLineNumbers=true&useMonospaceFont=false";
+
+
+/**
+ * Resizes the etherpad.
+ */
+function resize() {
+ if ($('#etherpad>iframe').length) {
+ var remoteVideos = $('#remoteVideos');
+ var availableHeight
+ = window.innerHeight - remoteVideos.outerHeight();
+ var availableWidth = UIUtil.getAvailableVideoWidth();
+
+ $('#etherpad>iframe').width(availableWidth);
+ $('#etherpad>iframe').height(availableHeight);
+ }
+}
+
+/**
+ * Creates the Etherpad button and adds it to the toolbar.
+ */
+function enableEtherpadButton() {
+ if (!$('#toolbar_button_etherpad').is(":visible"))
+ $('#toolbar_button_etherpad').css({display: 'inline-block'});
+}
+
+/**
+ * Creates the IFrame for the etherpad.
+ */
+function createIFrame() {
+ etherpadIFrame = VideoLayout.createEtherpadIframe(
+ domain + etherpadName + options, function() {
+
+ document.domain = document.domain;
+ bubbleIframeMouseMove(etherpadIFrame);
+ setTimeout(function() {
+ // the iframes inside of the etherpad are
+ // not yet loaded when the etherpad iframe is loaded
+ var outer = etherpadIFrame.
+ contentDocument.getElementsByName("ace_outer")[0];
+ bubbleIframeMouseMove(outer);
+ var inner = outer.
+ contentDocument.getElementsByName("ace_inner")[0];
+ bubbleIframeMouseMove(inner);
+ }, 2000);
+ });
+}
+
+function bubbleIframeMouseMove(iframe){
+ var existingOnMouseMove = iframe.contentWindow.onmousemove;
+ iframe.contentWindow.onmousemove = function(e){
+ if(existingOnMouseMove) existingOnMouseMove(e);
+ var evt = document.createEvent("MouseEvents");
+ var boundingClientRect = iframe.getBoundingClientRect();
+ evt.initMouseEvent(
+ "mousemove",
+ true, // bubbles
+ false, // not cancelable
+ window,
+ e.detail,
+ e.screenX,
+ e.screenY,
+ e.clientX + boundingClientRect.left,
+ e.clientY + boundingClientRect.top,
+ e.ctrlKey,
+ e.altKey,
+ e.shiftKey,
+ e.metaKey,
+ e.button,
+ null // no related element
+ );
+ iframe.dispatchEvent(evt);
+ };
+}
+
+
+var Etherpad = {
+ /**
+ * Initializes the etherpad.
+ */
+ init: function (name) {
+
+ if (config.etherpad_base && !etherpadName && name) {
+
+ domain = config.etherpad_base;
+
+ etherpadName = name;
+
+ enableEtherpadButton();
+
+ /**
+ * Resizes the etherpad, when the window is resized.
+ */
+ $(window).resize(function () {
+ resize();
+ });
+ }
+ },
+
+ /**
+ * Opens/hides the Etherpad.
+ */
+ toggleEtherpad: function (isPresentation) {
+ if (!etherpadIFrame)
+ createIFrame();
+
+
+ if(VideoLayout.getLargeVideoState() === "etherpad")
+ {
+ VideoLayout.setLargeVideoState("video");
+ }
+ else
+ {
+ VideoLayout.setLargeVideoState("etherpad");
+ }
+ resize();
+ }
+};
+
+module.exports = Etherpad;
+
+},{"../prezi/Prezi":18,"../util/UIUtil":33,"../videolayout/VideoLayout":39}],18:[function(require,module,exports){
+var ToolbarToggler = require("../toolbars/ToolbarToggler");
+var UIUtil = require("../util/UIUtil");
+var VideoLayout = require("../videolayout/VideoLayout");
+var messageHandler = require("../util/MessageHandler");
+var PreziPlayer = require("./PreziPlayer");
+
+var preziPlayer = null;
+
+
+/**
+ * Shows/hides a presentation.
+ */
+function setPresentationVisible(visible) {
+
+ if (visible) {
+ VideoLayout.setLargeVideoState("prezi");
+ }
+ else {
+ VideoLayout.setLargeVideoState("video");
+ }
+}
+
+var Prezi = {
+
+
+ /**
+ * Reloads the current presentation.
+ */
+ reloadPresentation: function() {
+ var iframe = document.getElementById(preziPlayer.options.preziId);
+ iframe.src = iframe.src;
+ },
+
+ /**
+ * Returns true if the presentation is visible, false -
+ * otherwise.
+ */
+ isPresentationVisible: function () {
+ return ($('#presentation>iframe') != null
+ && $('#presentation>iframe').css('opacity') == 1);
+ },
+
+ /**
+ * Opens the Prezi dialog, from which the user could choose a presentation
+ * to load.
+ */
+ openPreziDialog: function() {
+ var myprezi = APP.xmpp.getPrezi();
+ if (myprezi) {
+ messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
+ null,
+ "dialog.removePreziMsg",
+ null,
+ false,
+ "dialog.Remove",
+ function(e,v,m,f) {
+ if(v) {
+ APP.xmpp.removePreziFromPresence();
+ }
+ }
+ );
+ }
+ else if (preziPlayer != null) {
+ messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
+ null, "dialog.sharePreziMsg",
+ null,
+ false,
+ "dialog.Ok",
+ function(e,v,m,f) {
+ $.prompt.close();
+ }
+ );
+ }
+ else {
+ var html = APP.translation.generateTranslationHTML(
+ "dialog.sharePreziTitle");
+ var cancelButton = APP.translation.generateTranslationHTML(
+ "dialog.Cancel");
+ var shareButton = APP.translation.generateTranslationHTML(
+ "dialog.Share");
+ var backButton = APP.translation.generateTranslationHTML(
+ "dialog.Back");
+ var buttons = [];
+ var buttons1 = [];
+ // Cancel button to both states
+ buttons.push({title: cancelButton, value: false});
+ buttons1.push({title: cancelButton, value: false});
+ // Share button
+ buttons.push({title: shareButton, value: true});
+ // Back button
+ buttons1.push({title: backButton, value: true});
+ var linkError = APP.translation.generateTranslationHTML(
+ "dialog.preziLinkError");
+ var defaultUrl = APP.translation.translateString("defaultPreziLink",
+ {url: "http://prezi.com/wz7vhjycl7e6/my-prezi"});
+ var openPreziState = {
+ state0: {
+ html: '' + html + ' ' +
+ ' ',
+ persistent: false,
+ buttons: buttons,
+ focus: ':input:first',
+ defaultButton: 0,
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if(v)
+ {
+ var preziUrl = f.preziUrl;
+
+ if (preziUrl)
+ {
+ var urlValue
+ = encodeURI(UIUtil.escapeHtml(preziUrl));
+
+ if (urlValue.indexOf('http://prezi.com/') != 0
+ && urlValue.indexOf('https://prezi.com/') != 0)
+ {
+ $.prompt.goToState('state1');
+ return false;
+ }
+ else {
+ var presIdTmp = urlValue.substring(
+ urlValue.indexOf("prezi.com/") + 10);
+ if (!isAlphanumeric(presIdTmp)
+ || presIdTmp.indexOf('/') < 2) {
+ $.prompt.goToState('state1');
+ return false;
+ }
+ else {
+ APP.xmpp.addToPresence("prezi", urlValue);
+ $.prompt.close();
+ }
+ }
+ }
+ }
+ else
+ $.prompt.close();
+ }
+ },
+ state1: {
+ html: '' + html + ' ' +
+ linkError,
+ persistent: false,
+ buttons: buttons1,
+ focus: ':input:first',
+ defaultButton: 1,
+ submit: function (e, v, m, f) {
+ e.preventDefault();
+ if (v === 0)
+ $.prompt.close();
+ else
+ $.prompt.goToState('state0');
+ }
+ }
+ };
+ messageHandler.openDialogWithStates(openPreziState);
+ }
+ }
+
+};
+
+/**
+ * A new presentation has been added.
+ *
+ * @param event the event indicating the add of a presentation
+ * @param jid the jid from which the presentation was added
+ * @param presUrl url of the presentation
+ * @param currentSlide the current slide to which we should move
+ */
+function presentationAdded(event, jid, presUrl, currentSlide) {
+ console.log("presentation added", presUrl);
+
+ var presId = getPresentationId(presUrl);
+
+ var elementId = 'participant_'
+ + Strophe.getResourceFromJid(jid)
+ + '_' + presId;
+
+ VideoLayout.addPreziContainer(elementId);
+
+ var controlsEnabled = false;
+ if (jid === APP.xmpp.myJid())
+ controlsEnabled = true;
+
+ setPresentationVisible(true);
+ VideoLayout.setLargeVideoHover(
+ function (event) {
+ if (Prezi.isPresentationVisible()) {
+ var reloadButtonRight = window.innerWidth
+ - $('#presentation>iframe').offset().left
+ - $('#presentation>iframe').width();
+
+ $('#reloadPresentation').css({ right: reloadButtonRight,
+ display:'inline-block'});
+ }
+ },
+ function (event) {
+ if (!Prezi.isPresentationVisible())
+ $('#reloadPresentation').css({display:'none'});
+ else {
+ var e = event.toElement || event.relatedTarget;
+
+ if (e && e.id != 'reloadPresentation' && e.id != 'header')
+ $('#reloadPresentation').css({display:'none'});
+ }
+ });
+
+ preziPlayer = new PreziPlayer(
+ 'presentation',
+ {preziId: presId,
+ width: getPresentationWidth(),
+ height: getPresentationHeihgt(),
+ controls: controlsEnabled,
+ debug: true
+ });
+
+ $('#presentation>iframe').attr('id', preziPlayer.options.preziId);
+
+ preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
+ console.log("prezi status", event.value);
+ if (event.value == PreziPlayer.STATUS_CONTENT_READY) {
+ if (jid != APP.xmpp.myJid())
+ preziPlayer.flyToStep(currentSlide);
+ }
+ });
+
+ preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) {
+ console.log("event value", event.value);
+ APP.xmpp.addToPresence("preziSlide", event.value);
+ });
+
+ $("#" + elementId).css( 'background-image',
+ 'url(../images/avatarprezi.png)');
+ $("#" + elementId).click(
+ function () {
+ setPresentationVisible(true);
+ }
+ );
+};
+
+/**
+ * A presentation has been removed.
+ *
+ * @param event the event indicating the remove of a presentation
+ * @param jid the jid for which the presentation was removed
+ * @param the url of the presentation
+ */
+function presentationRemoved(event, jid, presUrl) {
+ console.log('presentation removed', presUrl);
+ var presId = getPresentationId(presUrl);
+ setPresentationVisible(false);
+ $('#participant_'
+ + Strophe.getResourceFromJid(jid)
+ + '_' + presId).remove();
+ $('#presentation>iframe').remove();
+ if (preziPlayer != null) {
+ preziPlayer.destroy();
+ preziPlayer = null;
+ }
+};
+
+/**
+ * Indicates if the given string is an alphanumeric string.
+ * Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
+ * purpose of checking URIs.
+ */
+function isAlphanumeric(unsafeText) {
+ var regex = /^[a-z0-9-_\/&\?=;]+$/i;
+ return regex.test(unsafeText);
+}
+
+/**
+ * Returns the presentation id from the given url.
+ */
+function getPresentationId (presUrl) {
+ var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
+ return presIdTmp.substring(0, presIdTmp.indexOf('/'));
+}
+
+/**
+ * Returns the presentation width.
+ */
+function getPresentationWidth() {
+ var availableWidth = UIUtil.getAvailableVideoWidth();
+ var availableHeight = getPresentationHeihgt();
+
+ var aspectRatio = 16.0 / 9.0;
+ if (availableHeight < availableWidth / aspectRatio) {
+ availableWidth = Math.floor(availableHeight * aspectRatio);
+ }
+ return availableWidth;
+}
+
+/**
+ * Returns the presentation height.
+ */
+function getPresentationHeihgt() {
+ var remoteVideos = $('#remoteVideos');
+ return window.innerHeight - remoteVideos.outerHeight();
+}
+
+/**
+ * Resizes the presentation iframe.
+ */
+function resize() {
+ if ($('#presentation>iframe')) {
+ $('#presentation>iframe').width(getPresentationWidth());
+ $('#presentation>iframe').height(getPresentationHeihgt());
+ }
+}
+
+/**
+ * Presentation has been removed.
+ */
+$(document).bind('presentationremoved.muc', presentationRemoved);
+
+/**
+ * Presentation has been added.
+ */
+$(document).bind('presentationadded.muc', presentationAdded);
+
+/*
+ * Indicates presentation slide change.
+ */
+$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
+ if (preziPlayer && preziPlayer.getCurrentStep() != current) {
+ preziPlayer.flyToStep(current);
+
+ var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
+ for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
+ preziPlayer.flyToStep(current, i);
+ }
+ }
+});
+
+$(window).resize(function () {
+ resize();
+});
+
+module.exports = Prezi;
+
+},{"../toolbars/ToolbarToggler":29,"../util/MessageHandler":31,"../util/UIUtil":33,"../videolayout/VideoLayout":39,"./PreziPlayer":19}],19:[function(require,module,exports){
+(function() {
+ "use strict";
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ window.PreziPlayer = (function() {
+
+ PreziPlayer.API_VERSION = 1;
+ PreziPlayer.CURRENT_STEP = 'currentStep';
+ PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
+ PreziPlayer.CURRENT_OBJECT = 'currentObject';
+ PreziPlayer.STATUS_LOADING = 'loading';
+ PreziPlayer.STATUS_READY = 'ready';
+ PreziPlayer.STATUS_CONTENT_READY = 'contentready';
+ PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
+ PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
+ PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
+ PreziPlayer.EVENT_STATUS = "statusChange";
+ PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
+ PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
+ PreziPlayer.domain = "https://prezi.com";
+ PreziPlayer.path = "/player/";
+ PreziPlayer.players = {};
+ PreziPlayer.binded_methods = ['changesHandler'];
+
+ PreziPlayer.createMultiplePlayers = function(optionArray){
+ for(var i=0; i 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;
+
+},{}],20:[function(require,module,exports){
+/* global require, $ */
+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");
+var LargeVideo = require("../videolayout/LargeVideo");
+
+/**
+ * Toggler for the chat, contact list, settings menu, etc..
+ */
+var PanelToggler = (function(my) {
+
+ var currentlyOpen = null;
+ var buttons = {
+ '#chatspace': '#chatBottomButton',
+ '#contactlist': '#contactListButton',
+ '#settingsmenu': '#toolbar_button_settings'
+ };
+
+ /**
+ * 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 (LargeVideo.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');
+ };
+
+ VideoLayout.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');};
+ VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
+
+ toggle(ContactList,
+ '#contactlist',
+ null,
+ function() {
+ ContactList.setVisualNotification(false);
+ },
+ null);
+ };
+
+ /**
+ * Opens / closes the settings menu
+ */
+ my.toggleSettingsMenu = function() {
+ VideoLayout.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":29,"../util/UIUtil":33,"../videolayout/LargeVideo":35,"../videolayout/VideoLayout":39,"./../../settings/Settings":47,"./chat/Chat":21,"./contactlist/ContactList":25,"./settings/SettingsMenu":26}],21:[function(require,module,exports){
+/* global APP, $, 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 = $('#toolbar_button_chat');
+ var bottomGlower = $('#chatBottomButton');
+
+ if (unreadMessages) {
+ unreadMsgElement.innerHTML = unreadMessages.toString();
+ unreadMsgBottomElement.innerHTML = unreadMessages.toString();
+
+ ToolbarToggler.dockToolbar(true);
+
+ var chatButtonElement
+ = document.getElementById('toolbar_button_chat');
+ 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(stamp) {
+ var now = (stamp? new Date(stamp): 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;
+ }
+ }
+ });
+
+ var usermsg = $('#usermsg');
+ 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, myjid, stamp) {
+ 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(stamp) +
+ '
' + '
' + 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":163,"../../toolbars/ToolbarToggler":29,"../../util/NicknameHandler":32,"../../util/UIUtil":33,"../SidePanelToggler":20,"./Commands":22,"./Replacement":23,"./smileys.json":24}],22:[function(require,module,exports){
+/* global APP, require */
+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 a 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":33}],23:[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
+};
+
+},{"./smileys.json":24}],24:[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
+ }
+}
+
+},{}],25:[function(require,module,exports){
+/* global $, APP, Strophe */
+var Avatar = require('../../avatar/Avatar');
+
+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) {
+ numberOfContacts += delta;
+ if (numberOfContacts === 1) {
+ // when the user is alone we don't show the number of participants
+ $("#numberOfParticipants").text('');
+ ContactList.setVisualNotification(false);
+ } else if (numberOfContacts > 1) {
+ ContactList.setVisualNotification(!ContactList.isVisible());
+ $("#numberOfParticipants").text(numberOfContacts);
+ } else {
+ console.error("Invalid number of participants: " + numberOfContacts);
+ }
+}
+
+/**
+ * Creates the avatar element.
+ *
+ * @return {object} the newly created avatar element
+ */
+function createAvatar(jid) {
+ var avatar = document.createElement('img');
+ avatar.className = "icon-avatar avatar";
+ avatar.src = Avatar.getContactListUrl(jid);
+
+ 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
+ */
+ ensureAddContact: function (peerJid) {
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contact = $('#contacts>li[id="' + resourceJid + '"]');
+
+ if (!contact || contact.length <= 0)
+ ContactList.addContact(peerJid);
+ },
+
+ /**
+ * Adds a contact for the given peer jid.
+ *
+ * @param peerJid the jid of the contact to add
+ */
+ addContact: function (peerJid) {
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ var contactlist = $('#contacts');
+
+ 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(peerJid));
+ newContact.appendChild(createDisplayNameParagraph("participant"));
+
+ if (resourceJid === APP.xmpp.myResource()) {
+ contactlist.prepend(newContact);
+ }
+ else {
+ contactlist.append(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 = $('#contacts>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 = $('#contacts>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 = $('#contacts #' + resourceJid + '>p');
+
+ if (contactName && displayName && displayName.length > 0)
+ contactName.html(displayName);
+ },
+
+ userAvatarChanged: function (resourceJid, contactListUrl) {
+ // set the avatar in the contact list
+ var contact = $('#' + resourceJid + '>img');
+ if (contact && contact.length > 0) {
+ contact.get(0).src = contactListUrl;
+ }
+
+ }
+};
+
+module.exports = ContactList;
+},{"../../avatar/Avatar":16}],26:[function(require,module,exports){
+/* global APP, $ */
+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 () {
+ var startMutedSelector = $("#startMutedOptions");
+ startMutedSelector.before(generateLanguagesSelectBox());
+ APP.translation.translateElement($("#languages_selectbox"));
+ $('#settingsmenu>input').keyup(function(event){
+ if(event.keyCode === 13) {//enter
+ SettingsMenu.update();
+ }
+ });
+
+ if (APP.xmpp.isModerator()) {
+ startMutedSelector.css("display", "block");
+ }
+ else {
+ startMutedSelector.css("display", "none");
+ }
+
+ $("#updateSettings").click(function () {
+ SettingsMenu.update();
+ });
+ },
+
+ onRoleChanged: function () {
+ if(APP.xmpp.isModerator()) {
+ $("#startMutedOptions").css("display", "block");
+ }
+ else {
+ $("#startMutedOptions").css("display", "none");
+ }
+ },
+
+ setStartMuted: function (audio, video) {
+ $("#startAudioMuted").attr("checked", audio);
+ $("#startVideoMuted").attr("checked", video);
+ },
+
+ 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);
+
+ var startAudioMuted = ($("#startAudioMuted").is(":checked"));
+ var startVideoMuted = ($("#startVideoMuted").is(":checked"));
+ APP.xmpp.addToPresence("startMuted",
+ [startAudioMuted, startVideoMuted]);
+
+ 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);
+ }
+ },
+ changeAvatar: function (thumbUrl) {
+ $('#avatar').get(0).src = thumbUrl;
+ }
+};
+
+
+module.exports = SettingsMenu;
+},{"../../../../service/translation/languages":168,"../../avatar/Avatar":16,"../../util/UIUtil":33,"./../../../settings/Settings":47}],27:[function(require,module,exports){
+/* global $ */
+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":20}],28:[function(require,module,exports){
+/* global APP, $, buttonClick, config, lockRoom, interfaceConfig, 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 recordingToaster = 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("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
+ return Toolbar.toggleFullScreen();
+ },
+ "toolbar_button_sip": function () {
+ return callSipButtonClicked();
+ },
+ "toolbar_button_dialpad": function () {
+ return dialpadButtonClicked();
+ },
+ "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.generateTranslationHTML(
+ "dialog.sessTerminated");
+ var msg = APP.translation.generateTranslationHTML(
+ "dialog.hungUp");
+ var button = APP.translation.generateTranslationHTML(
+ "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(predefinedToken) {
+ APP.xmpp.toggleRecording(function (callback) {
+ if (predefinedToken) {
+ callback(UIUtil.escapeHtml(predefinedToken));
+ return;
+ }
+
+ var msg = APP.translation.generateTranslationHTML(
+ "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);
+}
+
+/**
+ * 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 dialpadButtonClicked() {
+ //TODO show the dialpad box
+}
+
+function callSipButtonClicked() {
+ var defaultNumber
+ = config.defaultSipNumber ? config.defaultSipNumber : '';
+
+ var sipMsg = APP.translation.generateTranslationHTML(
+ "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, 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.isMUCJoined()) {
+ 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();
+ $('#inviteLinkRef').parent()
+ .find('button[value=true]').prop('disabled', false);
+ }
+ };
+
+ /**
+ * Disables and enables some of the buttons.
+ */
+ my.setupButtonsFromConfig = function () {
+ if (config.disablePrezi) {
+ $("#toolbar_button_prezi").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.generateTranslationHTML(
+ "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 (event) {
+ if (roomUrl) {
+ document.getElementById('inviteLinkRef').select();
+ } else {
+ if (event && event.target)
+ $(event.target)
+ .find('button[value=true]').prop('disabled', true);
+ }
+ }
+ );
+ };
+
+ /**
+ * Opens the settings dialog.
+ * FIXME: not used ?
+ */
+ my.openSettingsDialog = function () {
+ var settings1 = APP.translation.generateTranslationHTML(
+ "dialog.settings1");
+ var settings2 = APP.translation.generateTranslationHTML(
+ "dialog.settings2");
+ var settings3 = APP.translation.generateTranslationHTML(
+ "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 ($("#toolbar_button_security").hasClass("icon-security-locked"))
+ UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
+ };
+ /**
+ * Updates the lock button state to locked.
+ */
+ my.lockLockButton = function () {
+ if ($("#toolbar_button_security").hasClass("icon-security"))
+ UIUtil.buttonClick("#toolbar_button_security", "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) {
+ $('#toolbar_button_record').css({display: "inline-block"});
+ }
+ else {
+ $('#toolbar_button_record').css({display: "none"});
+ }
+ };
+
+ // Sets the state of the recording button
+ my.setRecordingButtonState = function (recordingState) {
+ var selector = $('#toolbar_button_record');
+
+ if (recordingState === 'on') {
+ selector.removeClass("icon-recEnable");
+ selector.addClass("icon-recEnable active");
+
+ $("#largeVideo").toggleClass("videoMessageFilter", true);
+ var recordOnKey = "recording.on";
+ $('#videoConnectionMessage').attr("data-i18n", recordOnKey);
+ $('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
+
+ setTimeout(function(){
+ $("#largeVideo").toggleClass("videoMessageFilter", false);
+ $('#videoConnectionMessage').css({display: "none"});
+ }, 1500);
+
+ recordingToaster = messageHandler.notify(null, "recording.toaster", null,
+ null, null, {timeOut: 0, closeButton: null, tapToDismiss: false});
+ } else if (recordingState === 'off') {
+ selector.removeClass("icon-recEnable active");
+ selector.addClass("icon-recEnable");
+
+ $("#largeVideo").toggleClass("videoMessageFilter", false);
+ $('#videoConnectionMessage').css({display: "none"});
+
+ if (recordingToaster)
+ messageHandler.remove(recordingToaster);
+
+ } else if (recordingState === 'pending') {
+ selector.removeClass("icon-recEnable active");
+ selector.addClass("icon-recEnable");
+
+ $("#largeVideo").toggleClass("videoMessageFilter", true);
+ var recordPendingKey = "recording.pending";
+ $('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
+ $('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
+ $('#videoConnectionMessage').css({display: "block"});
+ }
+ };
+
+ // checks whether recording is enabled and whether we have params to start automatically recording
+ my.checkAutoRecord = function () {
+ if (config.enableRecording && config.autoRecord) {
+ toggleRecording(config.autoRecordToken);
+ }
+ }
+
+ // Shows or hides SIP calls button
+ my.showSipCallButton = function (show) {
+ if (APP.xmpp.isSipGatewayEnabled() && show) {
+ $('#toolbar_button_sip').css({display: "inline-block"});
+ } else {
+ $('#toolbar_button_sip').css({display: "none"});
+ }
+ };
+
+ // Shows or hides the dialpad button
+ my.showDialPadButton = function (show) {
+ if (show) {
+ $('#toolbar_button_dialpad').css({display: "inline-block"});
+ } else {
+ $('#toolbar_button_dialpad').css({display: "none"});
+ }
+ };
+
+ /**
+ * Displays user authenticated identity name(login).
+ * @param authIdentity identity name to be displayed.
+ */
+ my.setAuthenticatedIdentity = function (authIdentity) {
+ if (authIdentity) {
+ var selector = $('#toolbar_auth_identity');
+ selector.css({display: "list-item"});
+ selector.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 = $("#toolbar_button_desktopsharing");
+ if (active) {
+ button.addClass("glow");
+ } else {
+ button.removeClass("glow");
+ }
+ };
+
+ return my;
+}(Toolbar || {}));
+
+module.exports = Toolbar;
+},{"../../../service/authentication/AuthenticationEvents":164,"../authentication/Authentication":14,"../etherpad/Etherpad":17,"../prezi/Prezi":18,"../side_pannels/SidePanelToggler":20,"../util/MessageHandler":31,"../util/UIUtil":33,"./BottomToolbar":27}],29:[function(require,module,exports){
+/* global APP, config, $, interfaceConfig, Moderator,
+ DesktopStreaming.showDesktopSharingButton */
+
+var toolbarTimeoutObject,
+ toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
+
+function showDesktopSharingButton() {
+ if (APP.desktopsharing.isDesktopSharingEnabled()) {
+ $('#toolbar_button_desktopsharing').css({display: "inline-block"});
+ } else {
+ $('#toolbar_button_desktopsharing').css({display: "none"});
+ }
+}
+
+/**
+ * Hides the toolbar.
+ */
+function hideToolbar() {
+ if(config.alwaysVisibleToolbar)
+ return;
+
+ 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 () {
+ if (interfaceConfig.filmStripOnly)
+ return;
+ 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 (interfaceConfig.filmStripOnly)
+ return;
+
+ 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;
+},{}],30:[function(require,module,exports){
+/* global $ */
+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 () {
+ if(!JitsiPopover.enabled)
+ return;
+ 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();
+ };
+
+ JitsiPopover.enabled = true;
+
+ return JitsiPopover;
+})();
+
+module.exports = JitsiPopover;
+},{}],31:[function(require,module,exports){
+/* global $, APP, jQuery, toastr */
+
+/**
+ * Flag for enable/disable of the notifications.
+ * @type {boolean}
+ */
+var notificationsEnabled = true;
+
+var messageHandler = (function(my) {
+
+ /**
+ * Shows a message to the user.
+ *
+ * @param titleKey the title of the message
+ * @param messageKey the text of the message
+ */
+ my.openMessageDialog = function(titleKey, messageKey) {
+ var title = null;
+ if(titleKey) {
+ title = APP.translation.generateTranslationHTML(titleKey);
+ }
+ var message = APP.translation.generateTranslationHTML(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.generateTranslationHTML(leftButtonKey);
+ buttons.push({ title: leftButton, value: true});
+
+ var cancelButton
+ = APP.translation.generateTranslationHTML("dialog.Cancel");
+ buttons.push({title: cancelButton, value: false});
+
+ var message = msgString, title = titleString;
+ if (titleKey) {
+ title = APP.translation.generateTranslationHTML(titleKey);
+ }
+ if (msgKey) {
+ message = APP.translation.generateTranslationHTML(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 {object} 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 titleKey the title of the message
+ * @param msgKey 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 titleKey the title of the message.
+ * @param msgKey the text of the message.
+ */
+ my.showError = function(titleKey, msgKey) {
+
+ if (!titleKey) {
+ titleKey = "dialog.oops";
+ }
+ if (!msgKey) {
+ msgKey = "dialog.defaultError";
+ }
+ messageHandler.openMessageDialog(titleKey, msgKey);
+ };
+
+ /**
+ * Displayes notification.
+ * @param displayName display name of the participant that is associated with the notification.
+ * @param displayNameKey the key from the language file for the display name.
+ * @param cls css class for the notification
+ * @param messageKey the key from the language file for the text of the message.
+ * @param messageArguments object with the arguments for the message.
+ * @param options object with language options.
+ */
+ my.notify = function(displayName, displayNameKey,
+ cls, messageKey, messageArguments, options) {
+ if(!notificationsEnabled)
+ return;
+ var displayNameSpan = '" + displayName;
+ } else {
+ displayNameSpan += "data-i18n='" + displayNameKey +
+ "'>" + APP.translation.translateString(displayNameKey);
+ }
+ displayNameSpan += " ";
+ return toastr.info(
+ displayNameSpan + ' ' +
+ '" +
+ APP.translation.translateString(messageKey,
+ messageArguments) +
+ ' ', null, options);
+ };
+
+ /**
+ * Removes the toaster.
+ * @param toasterElement
+ */
+ my.remove = function(toasterElement) {
+ toasterElement.remove();
+ };
+
+ /**
+ * Disables notifications.
+ */
+ my.disableNotifications = function () {
+ notificationsEnabled = false;
+ };
+
+ /**
+ * Enables notifications.
+ */
+ my.enableNotifications = function () {
+ notificationsEnabled = true;
+ };
+
+ return my;
+}(messageHandler || {}));
+
+module.exports = messageHandler;
+
+
+
+},{}],32:[function(require,module,exports){
+var UIEvents = require("../../../service/UI/UIEvents");
+
+var nickname = null;
+var eventEmitter = null;
+
+var NicknameHandler = {
+ 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 = NicknameHandler;
+},{"../../../service/UI/UIEvents":163}],33:[function(require,module,exports){
+/* global $ */
+/**
+ * Created by hristo on 12/22/14.
+ */
+module.exports = {
+ /**
+ * Returns the available video width.
+ */
+ getAvailableVideoWidth: function (isVisible) {
+ var PanelToggler = require("../side_pannels/SidePanelToggler");
+ if(typeof isVisible === "undefined" || isVisible === null)
+ isVisible = PanelToggler.isVisible();
+ var rightPanelWidth
+ = 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] * 0.3 + pixels[i+1] * 0.59 + pixels[i+2] * 0.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");
+ },
+
+ /**
+ * Inserts given child element as the first one into the container.
+ * @param container the container to which new child element will be added
+ * @param newChild the new element that will be inserted into the container
+ */
+ prependChild: function (container, newChild) {
+ var firstChild = container.childNodes[0];
+ if (firstChild) {
+ container.insertBefore(newChild, firstChild);
+ } else {
+ container.appendChild(newChild);
+ }
+ }
+};
+},{"../side_pannels/SidePanelToggler":20}],34:[function(require,module,exports){
+/* global APP, $ */
+var JitsiPopover = require("../util/JitsiPopover");
+
+/**
+ * Constructs new connection indicator.
+ * @param videoContainer the video container associated with the indicator.
+ * @constructor
+ */
+function ConnectionIndicator(videoContainer, jid) {
+ 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();
+}
+
+/**
+ * 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);
+ for(var ssrc in this.resolution) {
+ resolutionValue = this.resolution[ssrc];
+ }
+ }
+
+ 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.videoSpanId == "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.container.appendChild(
+ this.connectionIndicatorContainer);
+ this.popover = new JitsiPopover(
+ $("#" + this.videoContainer.videoSpanId + " > .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() {
+ if (this.connectionIndicatorContainer.parentNode) {
+ this.connectionIndicatorContainer.parentNode.removeChild(
+ this.connectionIndicatorContainer);
+ }
+ 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.videoContainer.updateIconPositions();
+ }
+ }
+ 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":32}],37:[function(require,module,exports){
-/* global $, APP, Strophe, interfaceConfig */
-var Avatar = require("../avatar/Avatar");
-var RTCBrowserType = require("../../RTC/RTCBrowserType");
-var UIUtil = require("../util/UIUtil");
-var UIEvents = require("../../../service/UI/UIEvents");
-var xmpp = require("../../xmpp/xmpp");
-var ToolbarToggler = require("../toolbars/ToolbarToggler");
-
-// FIXME: With Temasys we have to re-select everytime
-//var video = $('#largeVideo');
-
-var currentVideoWidth = null;
-var currentVideoHeight = null;
-// By default we use camera
-var getVideoSize = getCameraVideoSize;
-var getVideoPosition = getCameraVideoPosition;
-/**
- * The small video instance that is displayed in the large video
- * @type {SmallVideo}
- */
-var currentSmallVideo = null;
-/**
- * Indicates whether the large video is enabled.
- * @type {boolean}
- */
-var isEnabled = true;
-/**
- * Current large video state.
- * Possible values - video, prezi or etherpad.
- * @type {string}
- */
-var state = "video";
-
-/**
- * Returns the html element associated with the passed state of large video
- * @param state the state.
- * @returns {JQuery|*|jQuery|HTMLElement} the container.
- */
-function getContainerByState(state)
-{
- var selector = null;
- switch (state)
- {
- case "video":
- selector = "#largeVideo";
- break;
- case "etherpad":
- selector = "#etherpad>iframe";
- break;
- case "prezi":
- selector = "#presentation>iframe";
- break;
- }
- return (selector !== null)? $(selector) : null;
-}
-
-/**
- * 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,
- animate) {
- if (animate) {
- video.animate({
- width: width,
- height: height,
- top: verticalIndent,
- bottom: verticalIndent,
- left: horizontalIndent,
- right: horizontalIndent
- },
- {
- queue: false,
- duration: 500
- });
- } else {
- video.width(width);
- video.height(height);
- video.css({ top: verticalIndent + 'px',
- bottom: verticalIndent + 'px',
- left: horizontalIndent + 'px',
- right: horizontalIndent + 'px'});
- }
-
-}
-
-/**
- * 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];
-}
-
-
-/**
- * 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];
-}
-
-/**
- * Updates the src of the active speaker avatar
- * @param jid of the current active speaker
- */
-function updateActiveSpeakerAvatarSrc() {
- var avatar = $("#activeSpeakerAvatar")[0];
- var jid = currentSmallVideo.peerJid;
- var url = Avatar.getActiveSpeakerUrl(jid);
- if (avatar.src === url)
- return;
- if (jid) {
- avatar.src = url;
- currentSmallVideo.showAvatar();
- }
-}
-
-/**
- * Change the video source of the large video.
- * @param isVisible
- */
-function changeVideo(isVisible) {
-
- if (!currentSmallVideo) {
- console.error("Unable to change large video - no 'currentSmallVideo'");
- return;
- }
-
- updateActiveSpeakerAvatarSrc();
-
- APP.RTC.setVideoSrc($('#largeVideo')[0], currentSmallVideo.getSrc());
-
- var videoTransform = document.getElementById('largeVideo')
- .style.webkitTransform;
-
- var flipX = currentSmallVideo.flipX;
-
- if (flipX && videoTransform !== 'scaleX(-1)') {
- document.getElementById('largeVideo').style.webkitTransform =
- "scaleX(-1)";
- } else if (!flipX && videoTransform === 'scaleX(-1)') {
- document.getElementById('largeVideo').style.webkitTransform =
- "none";
- }
-
- var isDesktop = currentSmallVideo.getVideoType() === 'screen';
- // Change the way we'll be measuring and positioning large video
-
- getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
- getVideoPosition = isDesktop ? getDesktopVideoPosition :
- getCameraVideoPosition;
-
-
- // Only if the large video is currently visible.
- if (isVisible) {
- LargeVideo.VideoLayout.largeVideoUpdated(currentSmallVideo);
-
- $('#largeVideoWrapper').fadeIn(300);
- }
-}
-
-/**
- * Creates the html elements for the large video.
- */
-function createLargeVideoHTML()
-{
- var html = '';
- html += '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- ' jitsi.org' +
- ' '+
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- ' ' +
- '
' +
- '
';
- html += '
';
- $(html).prependTo("#videospace");
-
- if (interfaceConfig.SHOW_JITSI_WATERMARK) {
- var leftWatermarkDiv
- = $("#largeVideoContainer div[class='watermark leftwatermark']");
-
- leftWatermarkDiv.css({display: 'block'});
- leftWatermarkDiv.parent().get(0).href
- = interfaceConfig.JITSI_WATERMARK_LINK;
- }
-
- if (interfaceConfig.SHOW_BRAND_WATERMARK) {
- var rightWatermarkDiv
- = $("#largeVideoContainer div[class='watermark rightwatermark']");
-
- 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) {
- $("#largeVideoContainer>a[class='poweredby']").css({display: 'block'});
- }
-
- if (!RTCBrowserType.isIExplorer()) {
- $('#largeVideo').volume = 0;
- }
-}
-
-var LargeVideo = {
-
- init: function (VideoLayout, emitter) {
- if(!isEnabled)
- return;
- createLargeVideoHTML();
-
- this.VideoLayout = VideoLayout;
- this.eventEmitter = emitter;
- this.eventEmitter.emit(UIEvents.LARGEVIDEO_INIT);
- var self = this;
- // Listen for large video size updates
- var largeVideo = $('#largeVideo')[0];
- var onplaying = function (arg1, arg2, arg3) {
- // re-select
- if (RTCBrowserType.isTemasysPluginUsed())
- largeVideo = $('#largeVideo')[0];
- currentVideoWidth = largeVideo.videoWidth;
- currentVideoHeight = largeVideo.videoHeight;
- self.position(currentVideoWidth, currentVideoHeight);
- };
- largeVideo.onplaying = onplaying;
- },
- /**
- * Indicates if the large video is currently visible.
- *
- * @return true if visible, false - otherwise
- */
- isLargeVideoVisible: function() {
- return $('#largeVideo').is(':visible');
- },
- /**
- * Returns true if the user is currently displayed on large video.
- */
- isCurrentlyOnLarge: function (resourceJid) {
- return currentSmallVideo && resourceJid &&
- currentSmallVideo.getResourceJid() === resourceJid;
- },
- /**
- * Updates the large video with the given new video source.
- */
- updateLargeVideo: function (resourceJid, forceUpdate) {
- if(!isEnabled)
- return;
- var newSmallVideo = this.VideoLayout.getSmallVideo(resourceJid);
- console.log('hover in ' + resourceJid + ', video: ', newSmallVideo);
-
- if (!LargeVideo.isCurrentlyOnLarge(resourceJid) || forceUpdate) {
- $('#activeSpeaker').css('visibility', 'hidden');
-
- var oldSmallVideo = null;
- if (currentSmallVideo) {
- oldSmallVideo = currentSmallVideo;
- }
- currentSmallVideo = newSmallVideo;
-
- var oldJid = null;
- if (oldSmallVideo)
- oldJid = oldSmallVideo.peerJid;
- if (oldJid !== resourceJid) {
- // we want the notification to trigger even if userJid is undefined,
- // or null.
- this.eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, resourceJid);
- }
- // We are doing fadeOut/fadeIn animations on parent div which wraps
- // largeVideo, because when Temasys plugin is in use it replaces
- // elements with plugin tag. In Safari jQuery is
- // unable to store values on this plugin object which breaks all
- // animation effects performed on it directly.
- $('#largeVideoWrapper').fadeOut(300,
- changeVideo.bind($('#largeVideo'), this.isLargeVideoVisible()));
- } else {
- if (currentSmallVideo) {
- currentSmallVideo.showAvatar();
- }
- }
-
- },
-
- /**
- * Shows/hides the large video.
- */
- setLargeVideoVisible: function(isVisible) {
- if(!isEnabled)
- return;
- if (isVisible) {
- $('#largeVideo').css({visibility: 'visible'});
- $('.watermark').css({visibility: 'visible'});
- if(currentSmallVideo)
- currentSmallVideo.enableDominantSpeaker(true);
- }
- else {
- $('#largeVideo').css({visibility: 'hidden'});
- $('#activeSpeaker').css('visibility', 'hidden');
- $('.watermark').css({visibility: 'hidden'});
- if(currentSmallVideo)
- currentSmallVideo.enableDominantSpeaker(false);
- }
- },
- onVideoTypeChanged: function (resourceJid, newVideoType) {
- if (!isEnabled)
- return;
- if (LargeVideo.isCurrentlyOnLarge(resourceJid))
- {
- var isDesktop = newVideoType === 'screen';
- getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
- getVideoPosition = isDesktop ? getDesktopVideoPosition
- : getCameraVideoPosition;
- this.position(null, null, null, null, true);
- }
- },
- /**
- * Positions the large video.
- *
- * @param videoWidth the stream video width
- * @param videoHeight the stream video height
- */
- position: function (videoWidth, videoHeight,
- videoSpaceWidth, videoSpaceHeight, animate) {
- if(!isEnabled)
- return;
- if(!videoSpaceWidth)
- videoSpaceWidth = $('#videospace').width();
- if(!videoSpaceHeight)
- videoSpaceHeight = window.innerHeight;
-
- var videoSize = getVideoSize(videoWidth,
- videoHeight,
- videoSpaceWidth,
- videoSpaceHeight);
-
- var largeVideoWidth = videoSize[0];
- var largeVideoHeight = videoSize[1];
-
- var videoPosition = getVideoPosition(largeVideoWidth,
- largeVideoHeight,
- videoSpaceWidth,
- videoSpaceHeight);
-
- var horizontalIndent = videoPosition[0];
- var verticalIndent = videoPosition[1];
-
- positionVideo($('#largeVideoWrapper'),
- largeVideoWidth,
- largeVideoHeight,
- horizontalIndent, verticalIndent, animate);
- },
- /**
- * Resizes the large html elements.
- * @param animate boolean property that indicates whether the resize should be animated or not.
- * @param isChatVisible boolean property that indicates whether the chat area is displayed or not.
- * If that parameter is null the method will check the chat pannel visibility.
- * @param completeFunction a function to be called when the video space is resized
- * @returns {*[]} array with the current width and height values of the largeVideo html element.
- */
- resize: function (animate, isVisible, completeFunction) {
- if(!isEnabled)
- return;
- var availableHeight = window.innerHeight;
- var availableWidth = UIUtil.getAvailableVideoWidth(isVisible);
-
- if (availableWidth < 0 || availableHeight < 0) return;
-
- var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
- var top = availableHeight / 2 - avatarSize / 4 * 3;
- $('#activeSpeaker').css('top', top);
-
- this.VideoLayout.resizeVideoSpace(animate, isVisible, completeFunction);
- if(animate) {
- $('#largeVideoContainer').animate({
- width: availableWidth,
- height: availableHeight
- },
- {
- queue: false,
- duration: 500
- });
- } else {
- $('#largeVideoContainer').width(availableWidth);
- $('#largeVideoContainer').height(availableHeight);
- }
- return [availableWidth, availableHeight];
- },
- resizeVideoAreaAnimated: function (isVisible, completeFunction) {
- if(!isEnabled)
- return;
- var size = this.resize(true, isVisible, completeFunction);
- this.position(null, null, size[0], size[1], true);
- },
- getResourceJid: function () {
- return currentSmallVideo ? currentSmallVideo.getResourceJid() : null;
- },
- updateAvatar: function (resourceJid) {
- if(!isEnabled)
- return;
- if (resourceJid === this.getResourceJid()) {
- updateActiveSpeakerAvatarSrc();
- }
- },
- showAvatar: function (resourceJid, show) {
- if(!isEnabled)
- return;
- if(this.getResourceJid() === resourceJid && state === "video") {
- $("#largeVideo").css("visibility", show ? "hidden" : "visible");
- $('#activeSpeaker').css("visibility", show ? "visible" : "hidden");
- return true;
- }
- return false;
- },
- /**
- * Disables the large video
- */
- disable: function () {
- isEnabled = false;
- },
- /**
- * Enables the large video
- */
- enable: function () {
- isEnabled = true;
- },
- /**
- * Returns true if the video is enabled.
- */
- isEnabled: function () {
- return isEnabled;
- },
- /**
- * Creates the iframe used by the etherpad
- * @param src the value for src attribute
- * @param onloadHandler handler executed when the iframe loads it content
- * @returns {HTMLElement} the iframe
- */
- createEtherpadIframe: function (src, onloadHandler) {
- if(!isEnabled)
- return;
-
- var etherpadIFrame = document.createElement('iframe');
- etherpadIFrame.src = src;
- etherpadIFrame.frameBorder = 0;
- etherpadIFrame.scrolling = "no";
- etherpadIFrame.width = $('#largeVideoContainer').width() || 640;
- etherpadIFrame.height = $('#largeVideoContainer').height() || 480;
- etherpadIFrame.setAttribute('style', 'visibility: hidden;');
-
- document.getElementById('etherpad').appendChild(etherpadIFrame);
-
- etherpadIFrame.onload = onloadHandler;
-
- return etherpadIFrame;
- },
- /**
- * Changes the state of the large video.
- * Possible values - video, prezi, etherpad.
- * @param newState - the new state
- */
- setState: function (newState) {
- if(state === newState)
- return;
- var currentContainer = getContainerByState(state);
- if(!currentContainer)
- return;
-
- var self = this;
- var oldState = state;
-
- switch (newState)
- {
- case "etherpad":
- $('#activeSpeaker').css('visibility', 'hidden');
- currentContainer.fadeOut(300, function () {
- if (oldState === "prezi") {
- currentContainer.css({opacity: '0'});
- $('#reloadPresentation').css({display: 'none'});
- }
- else {
- self.setLargeVideoVisible(false);
- }
- });
-
- $('#etherpad>iframe').fadeIn(300, function () {
- document.body.style.background = '#eeeeee';
- $('#etherpad>iframe').css({visibility: 'visible'});
- $('#etherpad').css({zIndex: 2});
- });
- break;
- case "prezi":
- var prezi = $('#presentation>iframe');
- currentContainer.fadeOut(300, function() {
- document.body.style.background = 'black';
- });
- prezi.fadeIn(300, function() {
- prezi.css({opacity:'1'});
- ToolbarToggler.dockToolbar(true);//fix that
- self.setLargeVideoVisible(false);
- $('#etherpad>iframe').css({visibility: 'hidden'});
- $('#etherpad').css({zIndex: 0});
- });
- $('#activeSpeaker').css('visibility', 'hidden');
- break;
-
- case "video":
- currentContainer.fadeOut(300, function () {
- $('#presentation>iframe').css({opacity:'0'});
- $('#reloadPresentation').css({display:'none'});
- $('#etherpad>iframe').css({visibility: 'hidden'});
- $('#etherpad').css({zIndex: 0});
- document.body.style.background = 'black';
- ToolbarToggler.dockToolbar(false);//fix that
- });
- $('#largeVideoWrapper').fadeIn(300, function () {
- self.setLargeVideoVisible(true);
- });
- break;
- }
-
- state = newState;
-
- },
- /**
- * Returns the current state of the large video.
- * @returns {string} the current state - video, prezi or etherpad.
- */
- getState: function () {
- return state;
- },
- /**
- * Sets hover handlers for the large video container div.
- *
- * @param inHandler
- * @param outHandler
- */
- setHover: function(inHandler, outHandler)
- {
- $('#largeVideoContainer').hover(inHandler, outHandler);
- },
-
- /**
- * Enables/disables the filter indicating a video problem to the user.
- *
- * @param enable true to enable, false to disable
- */
- enableVideoProblemFilter: function (enable) {
- $("#largeVideo").toggleClass("videoProblemFilter", enable);
- }
-};
-
+},{"../util/JitsiPopover":30}],35:[function(require,module,exports){
+/* global $, APP, Strophe, interfaceConfig */
+var Avatar = require("../avatar/Avatar");
+var RTCBrowserType = require("../../RTC/RTCBrowserType");
+var UIUtil = require("../util/UIUtil");
+var UIEvents = require("../../../service/UI/UIEvents");
+var xmpp = require("../../xmpp/xmpp");
+var ToolbarToggler = require("../toolbars/ToolbarToggler");
+
+// FIXME: With Temasys we have to re-select everytime
+//var video = $('#largeVideo');
+
+var currentVideoWidth = null;
+var currentVideoHeight = null;
+// By default we use camera
+var getVideoSize = getCameraVideoSize;
+var getVideoPosition = getCameraVideoPosition;
+/**
+ * The small video instance that is displayed in the large video
+ * @type {SmallVideo}
+ */
+var currentSmallVideo = null;
+/**
+ * Indicates whether the large video is enabled.
+ * @type {boolean}
+ */
+var isEnabled = true;
+/**
+ * Current large video state.
+ * Possible values - video, prezi or etherpad.
+ * @type {string}
+ */
+var state = "video";
+
+/**
+ * Returns the html element associated with the passed state of large video
+ * @param state the state.
+ * @returns {JQuery|*|jQuery|HTMLElement} the container.
+ */
+function getContainerByState(state)
+{
+ var selector = null;
+ switch (state)
+ {
+ case "video":
+ selector = "#largeVideo";
+ break;
+ case "etherpad":
+ selector = "#etherpad>iframe";
+ break;
+ case "prezi":
+ selector = "#presentation>iframe";
+ break;
+ }
+ return (selector !== null)? $(selector) : null;
+}
+
+/**
+ * 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,
+ animate) {
+ if (animate) {
+ video.animate({
+ width: width,
+ height: height,
+ top: verticalIndent,
+ bottom: verticalIndent,
+ left: horizontalIndent,
+ right: horizontalIndent
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+ } else {
+ video.width(width);
+ video.height(height);
+ video.css({ top: verticalIndent + 'px',
+ bottom: verticalIndent + 'px',
+ left: horizontalIndent + 'px',
+ right: horizontalIndent + 'px'});
+ }
+
+}
+
+/**
+ * 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];
+}
+
+
+/**
+ * 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];
+}
+
+/**
+ * Updates the src of the active speaker avatar
+ * @param jid of the current active speaker
+ */
+function updateActiveSpeakerAvatarSrc() {
+ var avatar = $("#activeSpeakerAvatar")[0];
+ var jid = currentSmallVideo.peerJid;
+ var url = Avatar.getActiveSpeakerUrl(jid);
+ if (avatar.src === url)
+ return;
+ if (jid) {
+ avatar.src = url;
+ currentSmallVideo.showAvatar();
+ }
+}
+
+/**
+ * Change the video source of the large video.
+ * @param isVisible
+ */
+function changeVideo(isVisible) {
+
+ if (!currentSmallVideo) {
+ console.error("Unable to change large video - no 'currentSmallVideo'");
+ return;
+ }
+
+ updateActiveSpeakerAvatarSrc();
+
+ APP.RTC.setVideoSrc($('#largeVideo')[0], currentSmallVideo.getSrc());
+
+ var videoTransform = document.getElementById('largeVideo')
+ .style.webkitTransform;
+
+ var flipX = currentSmallVideo.flipX;
+
+ if (flipX && videoTransform !== 'scaleX(-1)') {
+ document.getElementById('largeVideo').style.webkitTransform =
+ "scaleX(-1)";
+ } else if (!flipX && videoTransform === 'scaleX(-1)') {
+ document.getElementById('largeVideo').style.webkitTransform =
+ "none";
+ }
+
+ var isDesktop = currentSmallVideo.getVideoType() === 'screen';
+ // Change the way we'll be measuring and positioning large video
+
+ getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
+ getVideoPosition = isDesktop ? getDesktopVideoPosition :
+ getCameraVideoPosition;
+
+
+ // Only if the large video is currently visible.
+ if (isVisible) {
+ LargeVideo.VideoLayout.largeVideoUpdated(currentSmallVideo);
+
+ $('#largeVideoWrapper').fadeIn(300);
+ }
+}
+
+/**
+ * Creates the html elements for the large video.
+ */
+function createLargeVideoHTML()
+{
+ var html = '';
+ html += '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ ' jitsi.org' +
+ ' '+
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ ' ' +
+ '
' +
+ '
';
+ html += '
';
+ $(html).prependTo("#videospace");
+
+ if (interfaceConfig.SHOW_JITSI_WATERMARK) {
+ var leftWatermarkDiv
+ = $("#largeVideoContainer div[class='watermark leftwatermark']");
+
+ leftWatermarkDiv.css({display: 'block'});
+ leftWatermarkDiv.parent().get(0).href
+ = interfaceConfig.JITSI_WATERMARK_LINK;
+ }
+
+ if (interfaceConfig.SHOW_BRAND_WATERMARK) {
+ var rightWatermarkDiv
+ = $("#largeVideoContainer div[class='watermark rightwatermark']");
+
+ 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) {
+ $("#largeVideoContainer>a[class='poweredby']").css({display: 'block'});
+ }
+
+ if (!RTCBrowserType.isIExplorer()) {
+ $('#largeVideo').volume = 0;
+ }
+}
+
+var LargeVideo = {
+
+ init: function (VideoLayout, emitter) {
+ if(!isEnabled)
+ return;
+ createLargeVideoHTML();
+
+ this.VideoLayout = VideoLayout;
+ this.eventEmitter = emitter;
+ this.eventEmitter.emit(UIEvents.LARGEVIDEO_INIT);
+ var self = this;
+ // Listen for large video size updates
+ var largeVideo = $('#largeVideo')[0];
+ var onplaying = function (arg1, arg2, arg3) {
+ // re-select
+ if (RTCBrowserType.isTemasysPluginUsed())
+ largeVideo = $('#largeVideo')[0];
+ currentVideoWidth = largeVideo.videoWidth;
+ currentVideoHeight = largeVideo.videoHeight;
+ self.position(currentVideoWidth, currentVideoHeight);
+ };
+ largeVideo.onplaying = onplaying;
+ },
+ /**
+ * Indicates if the large video is currently visible.
+ *
+ * @return true if visible, false - otherwise
+ */
+ isLargeVideoVisible: function() {
+ return $('#largeVideo').is(':visible');
+ },
+ /**
+ * Returns true if the user is currently displayed on large video.
+ */
+ isCurrentlyOnLarge: function (resourceJid) {
+ return currentSmallVideo && resourceJid &&
+ currentSmallVideo.getResourceJid() === resourceJid;
+ },
+ /**
+ * Updates the large video with the given new video source.
+ */
+ updateLargeVideo: function (resourceJid, forceUpdate) {
+ if(!isEnabled)
+ return;
+ var newSmallVideo = this.VideoLayout.getSmallVideo(resourceJid);
+ console.log('hover in ' + resourceJid + ', video: ', newSmallVideo);
+
+ if (!LargeVideo.isCurrentlyOnLarge(resourceJid) || forceUpdate) {
+ $('#activeSpeaker').css('visibility', 'hidden');
+
+ var oldSmallVideo = null;
+ if (currentSmallVideo) {
+ oldSmallVideo = currentSmallVideo;
+ }
+ currentSmallVideo = newSmallVideo;
+
+ var oldJid = null;
+ if (oldSmallVideo)
+ oldJid = oldSmallVideo.peerJid;
+ if (oldJid !== resourceJid) {
+ // we want the notification to trigger even if userJid is undefined,
+ // or null.
+ this.eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, resourceJid);
+ }
+ // We are doing fadeOut/fadeIn animations on parent div which wraps
+ // largeVideo, because when Temasys plugin is in use it replaces
+ // elements with plugin tag. In Safari jQuery is
+ // unable to store values on this plugin object which breaks all
+ // animation effects performed on it directly.
+ $('#largeVideoWrapper').fadeOut(300,
+ changeVideo.bind($('#largeVideo'), this.isLargeVideoVisible()));
+ } else {
+ if (currentSmallVideo) {
+ currentSmallVideo.showAvatar();
+ }
+ }
+
+ },
+
+ /**
+ * Shows/hides the large video.
+ */
+ setLargeVideoVisible: function(isVisible) {
+ if(!isEnabled)
+ return;
+ if (isVisible) {
+ $('#largeVideo').css({visibility: 'visible'});
+ $('.watermark').css({visibility: 'visible'});
+ if(currentSmallVideo)
+ currentSmallVideo.enableDominantSpeaker(true);
+ }
+ else {
+ $('#largeVideo').css({visibility: 'hidden'});
+ $('#activeSpeaker').css('visibility', 'hidden');
+ $('.watermark').css({visibility: 'hidden'});
+ if(currentSmallVideo)
+ currentSmallVideo.enableDominantSpeaker(false);
+ }
+ },
+ onVideoTypeChanged: function (resourceJid, newVideoType) {
+ if (!isEnabled)
+ return;
+ if (LargeVideo.isCurrentlyOnLarge(resourceJid))
+ {
+ var isDesktop = newVideoType === 'screen';
+ getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
+ getVideoPosition = isDesktop ? getDesktopVideoPosition
+ : getCameraVideoPosition;
+ this.position(null, null, null, null, true);
+ }
+ },
+ /**
+ * Positions the large video.
+ *
+ * @param videoWidth the stream video width
+ * @param videoHeight the stream video height
+ */
+ position: function (videoWidth, videoHeight,
+ videoSpaceWidth, videoSpaceHeight, animate) {
+ if(!isEnabled)
+ return;
+ if(!videoSpaceWidth)
+ videoSpaceWidth = $('#videospace').width();
+ if(!videoSpaceHeight)
+ videoSpaceHeight = window.innerHeight;
+
+ var videoSize = getVideoSize(videoWidth,
+ videoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight);
+
+ var largeVideoWidth = videoSize[0];
+ var largeVideoHeight = videoSize[1];
+
+ var videoPosition = getVideoPosition(largeVideoWidth,
+ largeVideoHeight,
+ videoSpaceWidth,
+ videoSpaceHeight);
+
+ var horizontalIndent = videoPosition[0];
+ var verticalIndent = videoPosition[1];
+
+ positionVideo($('#largeVideoWrapper'),
+ largeVideoWidth,
+ largeVideoHeight,
+ horizontalIndent, verticalIndent, animate);
+ },
+ /**
+ * Resizes the large html elements.
+ * @param animate boolean property that indicates whether the resize should be animated or not.
+ * @param isChatVisible boolean property that indicates whether the chat area is displayed or not.
+ * If that parameter is null the method will check the chat pannel visibility.
+ * @param completeFunction a function to be called when the video space is resized
+ * @returns {*[]} array with the current width and height values of the largeVideo html element.
+ */
+ resize: function (animate, isVisible, completeFunction) {
+ if(!isEnabled)
+ return;
+ var availableHeight = window.innerHeight;
+ var availableWidth = UIUtil.getAvailableVideoWidth(isVisible);
+
+ if (availableWidth < 0 || availableHeight < 0) return;
+
+ var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
+ var top = availableHeight / 2 - avatarSize / 4 * 3;
+ $('#activeSpeaker').css('top', top);
+
+ this.VideoLayout.resizeVideoSpace(animate, isVisible, completeFunction);
+ if(animate) {
+ $('#largeVideoContainer').animate({
+ width: availableWidth,
+ height: availableHeight
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+ } else {
+ $('#largeVideoContainer').width(availableWidth);
+ $('#largeVideoContainer').height(availableHeight);
+ }
+ return [availableWidth, availableHeight];
+ },
+ resizeVideoAreaAnimated: function (isVisible, completeFunction) {
+ if(!isEnabled)
+ return;
+ var size = this.resize(true, isVisible, completeFunction);
+ this.position(null, null, size[0], size[1], true);
+ },
+ getResourceJid: function () {
+ return currentSmallVideo ? currentSmallVideo.getResourceJid() : null;
+ },
+ updateAvatar: function (resourceJid) {
+ if(!isEnabled)
+ return;
+ if (resourceJid === this.getResourceJid()) {
+ updateActiveSpeakerAvatarSrc();
+ }
+ },
+ showAvatar: function (resourceJid, show) {
+ if(!isEnabled)
+ return;
+ if(this.getResourceJid() === resourceJid && state === "video") {
+ $("#largeVideo").css("visibility", show ? "hidden" : "visible");
+ $('#activeSpeaker').css("visibility", show ? "visible" : "hidden");
+ return true;
+ }
+ return false;
+ },
+ /**
+ * Disables the large video
+ */
+ disable: function () {
+ isEnabled = false;
+ },
+ /**
+ * Enables the large video
+ */
+ enable: function () {
+ isEnabled = true;
+ },
+ /**
+ * Returns true if the video is enabled.
+ */
+ isEnabled: function () {
+ return isEnabled;
+ },
+ /**
+ * Creates the iframe used by the etherpad
+ * @param src the value for src attribute
+ * @param onloadHandler handler executed when the iframe loads it content
+ * @returns {HTMLElement} the iframe
+ */
+ createEtherpadIframe: function (src, onloadHandler) {
+ if(!isEnabled)
+ return;
+
+ var etherpadIFrame = document.createElement('iframe');
+ etherpadIFrame.src = src;
+ etherpadIFrame.frameBorder = 0;
+ etherpadIFrame.scrolling = "no";
+ etherpadIFrame.width = $('#largeVideoContainer').width() || 640;
+ etherpadIFrame.height = $('#largeVideoContainer').height() || 480;
+ etherpadIFrame.setAttribute('style', 'visibility: hidden;');
+
+ document.getElementById('etherpad').appendChild(etherpadIFrame);
+
+ etherpadIFrame.onload = onloadHandler;
+
+ return etherpadIFrame;
+ },
+ /**
+ * Changes the state of the large video.
+ * Possible values - video, prezi, etherpad.
+ * @param newState - the new state
+ */
+ setState: function (newState) {
+ if(state === newState)
+ return;
+ var currentContainer = getContainerByState(state);
+ if(!currentContainer)
+ return;
+
+ var self = this;
+ var oldState = state;
+
+ switch (newState)
+ {
+ case "etherpad":
+ $('#activeSpeaker').css('visibility', 'hidden');
+ currentContainer.fadeOut(300, function () {
+ if (oldState === "prezi") {
+ currentContainer.css({opacity: '0'});
+ $('#reloadPresentation').css({display: 'none'});
+ }
+ else {
+ self.setLargeVideoVisible(false);
+ }
+ });
+
+ $('#etherpad>iframe').fadeIn(300, function () {
+ document.body.style.background = '#eeeeee';
+ $('#etherpad>iframe').css({visibility: 'visible'});
+ $('#etherpad').css({zIndex: 2});
+ });
+ break;
+ case "prezi":
+ var prezi = $('#presentation>iframe');
+ currentContainer.fadeOut(300, function() {
+ document.body.style.background = 'black';
+ });
+ prezi.fadeIn(300, function() {
+ prezi.css({opacity:'1'});
+ ToolbarToggler.dockToolbar(true);//fix that
+ self.setLargeVideoVisible(false);
+ $('#etherpad>iframe').css({visibility: 'hidden'});
+ $('#etherpad').css({zIndex: 0});
+ });
+ $('#activeSpeaker').css('visibility', 'hidden');
+ break;
+
+ case "video":
+ currentContainer.fadeOut(300, function () {
+ $('#presentation>iframe').css({opacity:'0'});
+ $('#reloadPresentation').css({display:'none'});
+ $('#etherpad>iframe').css({visibility: 'hidden'});
+ $('#etherpad').css({zIndex: 0});
+ document.body.style.background = 'black';
+ ToolbarToggler.dockToolbar(false);//fix that
+ });
+ $('#largeVideoWrapper').fadeIn(300, function () {
+ self.setLargeVideoVisible(true);
+ });
+ break;
+ }
+
+ state = newState;
+
+ },
+ /**
+ * Returns the current state of the large video.
+ * @returns {string} the current state - video, prezi or etherpad.
+ */
+ getState: function () {
+ return state;
+ },
+ /**
+ * Sets hover handlers for the large video container div.
+ *
+ * @param inHandler
+ * @param outHandler
+ */
+ setHover: function(inHandler, outHandler)
+ {
+ $('#largeVideoContainer').hover(inHandler, outHandler);
+ },
+
+ /**
+ * Enables/disables the filter indicating a video problem to the user.
+ *
+ * @param enable true to enable, false to disable
+ */
+ enableVideoProblemFilter: function (enable) {
+ $("#largeVideo").toggleClass("videoProblemFilter", enable);
+ }
+};
+
module.exports = LargeVideo;
-},{"../../../service/UI/UIEvents":160,"../../RTC/RTCBrowserType":10,"../../xmpp/xmpp":70,"../avatar/Avatar":18,"../toolbars/ToolbarToggler":31,"../util/UIUtil":35}],38:[function(require,module,exports){
-/* global $, interfaceConfig, APP */
-var SmallVideo = require("./SmallVideo");
-var ConnectionIndicator = require("./ConnectionIndicator");
-var NicknameHandler = require("../util/NicknameHandler");
-var UIUtil = require("../util/UIUtil");
-var LargeVideo = require("./LargeVideo");
-var RTCBrowserType = require("../../RTC/RTCBrowserType");
-
-function LocalVideo(VideoLayout) {
- this.videoSpanId = "localVideoContainer";
- this.container = $("#localVideoContainer").get(0);
- this.VideoLayout = VideoLayout;
- this.flipX = true;
- this.isLocal = true;
- this.peerJid = null;
-}
-
-LocalVideo.prototype = Object.create(SmallVideo.prototype);
-LocalVideo.prototype.constructor = LocalVideo;
-
-/**
- * Creates the edit display name button.
- *
- * @returns {object} the edit button
- */
-function createEditDisplayNameButton() {
- var editButton = document.createElement('a');
- editButton.className = 'displayname';
- UIUtil.setTooltip(editButton,
- "videothumbnail.editnickname",
- "top");
- editButton.innerHTML = ' ';
-
- return editButton;
-}
-
-/**
- * Sets the display name for the given video span id.
- */
-LocalVideo.prototype.setDisplayName = function(displayName, key) {
- if (!this.container) {
- console.warn(
- "Unable to set displayName - " + this.videoSpanId +
- " does not exist");
- return;
- }
-
- var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
- var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
- interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
-
- var meHTML;
- // If we already have a display name for this video.
- if (nameSpan.length > 0) {
- if (nameSpan.text() !== displayName) {
- if (displayName && displayName.length > 0) {
- meHTML = APP.translation.generateTranslationHTML("me");
- $('#localDisplayName').html(displayName + ' (' + meHTML + ')');
- } else {
- $('#localDisplayName').html(defaultLocalDisplayName);
- }
- }
- } else {
- var editButton = createEditDisplayNameButton();
-
- nameSpan = document.createElement('span');
- nameSpan.className = 'displayname';
- $('#' + this.videoSpanId)[0].appendChild(nameSpan);
-
-
- if (displayName && displayName.length > 0) {
- meHTML = APP.translation.generateTranslationHTML("me");
- nameSpan.innerHTML = displayName + meHTML;
- }
- else {
- nameSpan.innerHTML = defaultLocalDisplayName;
- }
-
-
- nameSpan.id = 'localDisplayName';
- this.container.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);
-
- this.container.appendChild(editableText);
-
- var self = this;
- $('#localVideoContainer .displayname')
- .bind("click", function (e) {
-
- var editDisplayName = $('#editDisplayName');
- e.preventDefault();
- e.stopPropagation();
- $('#localDisplayName').hide();
- editDisplayName.show();
- editDisplayName.focus();
- editDisplayName.select();
-
- editDisplayName.one("focusout", function (e) {
- self.VideoLayout.inputDisplayNameHandler(this.value);
- });
-
- editDisplayName.on('keydown', function (e) {
- if (e.keyCode === 13) {
- e.preventDefault();
- self.VideoLayout.inputDisplayNameHandler(this.value);
- }
- });
- });
- }
-};
-
-LocalVideo.prototype.inputDisplayNameHandler = function (name) {
- NicknameHandler.setNickname(name);
-
- var localDisplayName = $('#localDisplayName');
- if (!localDisplayName.is(":visible")) {
- if (NicknameHandler.getNickname()) {
- var meHTML = APP.translation.generateTranslationHTML("me");
- localDisplayName.html(NicknameHandler.getNickname() + " (" +
- meHTML + ")");
- } else {
- var defaultHTML = APP.translation.generateTranslationHTML(
- interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
- localDisplayName .html(defaultHTML);
- }
- localDisplayName.show();
- }
-
- $('#editDisplayName').hide();
-};
-
-LocalVideo.prototype.createConnectionIndicator = function() {
- if(this.connectionIndicator)
- return;
-
- this.connectionIndicator = new ConnectionIndicator(this, null);
-};
-
-LocalVideo.prototype.changeVideo = function (stream, isMuted) {
- var self = this;
-
- function localVideoClick(event) {
- // FIXME: with Temasys plugin event arg is not an event, but
- // the clicked object itself, so we have to skip this call
- if (event.stopPropagation) {
- event.stopPropagation();
- }
- self.VideoLayout.handleVideoThumbClicked(
- false,
- APP.xmpp.myResource());
- }
-
- var localVideoContainerSelector = $('#localVideoContainer');
- localVideoContainerSelector.off('click');
- localVideoContainerSelector.on('click', localVideoClick);
-
- // Add hover handler
- localVideoContainerSelector.hover(
- function() {
- self.showDisplayName(true);
- },
- function() {
- if (!LargeVideo.isLargeVideoVisible() ||
- !LargeVideo.isCurrentlyOnLarge(self.getResourceJid())) {
- self.showDisplayName(false);
- }
- }
- );
-
- if(isMuted) {
- APP.UI.setVideoMute(true);
- return;
- }
- this.flipX = stream.videoType != "screen";
- var localVideo = document.createElement('video');
- localVideo.id = 'localVideo_' +
- APP.RTC.getStreamID(stream.getOriginalStream());
- if (!RTCBrowserType.isIExplorer()) {
- localVideo.autoplay = true;
- localVideo.volume = 0; // is it required if audio is separated ?
- }
- localVideo.oncontextmenu = function () { return false; };
-
- var localVideoContainer = document.getElementById('localVideoWrapper');
- // Put the new video always in front
- UIUtil.prependChild(localVideoContainer, localVideo);
-
- var localVideoSelector = $('#' + localVideo.id);
-
- // Add click handler to both video and video wrapper elements in case
- // there's no video.
-
- // onclick has to be used with Temasys plugin
- localVideo.onclick = localVideoClick;
-
- if (this.flipX) {
- localVideoSelector.addClass("flipVideoX");
- }
-
- // Attach WebRTC stream
- APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream());
-
- // Add stream ended handler
- stream.getOriginalStream().onended = function () {
- // We have to re-select after attach when Temasys plugin is used,
- // because element is replaced with
- localVideo = $('#' + localVideo.id)[0];
- localVideoContainer.removeChild(localVideo);
- self.VideoLayout.updateRemovedVideo(APP.xmpp.myResource());
- };
-};
-
-LocalVideo.prototype.joined = function (jid) {
- this.peerJid = jid;
-};
-
-LocalVideo.prototype.getResourceJid = function () {
- var myResource = APP.xmpp.myResource();
- if (!myResource) {
- console.error("Requested local resource before we're in the MUC");
- }
- return myResource;
-};
-
+},{"../../../service/UI/UIEvents":163,"../../RTC/RTCBrowserType":8,"../../xmpp/xmpp":68,"../avatar/Avatar":16,"../toolbars/ToolbarToggler":29,"../util/UIUtil":33}],36:[function(require,module,exports){
+/* global $, interfaceConfig, APP */
+var SmallVideo = require("./SmallVideo");
+var ConnectionIndicator = require("./ConnectionIndicator");
+var NicknameHandler = require("../util/NicknameHandler");
+var UIUtil = require("../util/UIUtil");
+var LargeVideo = require("./LargeVideo");
+var RTCBrowserType = require("../../RTC/RTCBrowserType");
+
+function LocalVideo(VideoLayout) {
+ this.videoSpanId = "localVideoContainer";
+ this.container = $("#localVideoContainer").get(0);
+ this.VideoLayout = VideoLayout;
+ this.flipX = true;
+ this.isLocal = true;
+ this.peerJid = null;
+}
+
+LocalVideo.prototype = Object.create(SmallVideo.prototype);
+LocalVideo.prototype.constructor = LocalVideo;
+
+/**
+ * Creates the edit display name button.
+ *
+ * @returns {object} the edit button
+ */
+function createEditDisplayNameButton() {
+ var editButton = document.createElement('a');
+ editButton.className = 'displayname';
+ UIUtil.setTooltip(editButton,
+ "videothumbnail.editnickname",
+ "top");
+ editButton.innerHTML = ' ';
+
+ return editButton;
+}
+
+/**
+ * Sets the display name for the given video span id.
+ */
+LocalVideo.prototype.setDisplayName = function(displayName, key) {
+ if (!this.container) {
+ console.warn(
+ "Unable to set displayName - " + this.videoSpanId +
+ " does not exist");
+ return;
+ }
+
+ var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
+ var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
+ interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
+
+ var meHTML;
+ // If we already have a display name for this video.
+ if (nameSpan.length > 0) {
+ if (nameSpan.text() !== displayName) {
+ if (displayName && displayName.length > 0) {
+ meHTML = APP.translation.generateTranslationHTML("me");
+ $('#localDisplayName').html(displayName + ' (' + meHTML + ')');
+ } else {
+ $('#localDisplayName').html(defaultLocalDisplayName);
+ }
+ }
+ } else {
+ var editButton = createEditDisplayNameButton();
+
+ nameSpan = document.createElement('span');
+ nameSpan.className = 'displayname';
+ $('#' + this.videoSpanId)[0].appendChild(nameSpan);
+
+
+ if (displayName && displayName.length > 0) {
+ meHTML = APP.translation.generateTranslationHTML("me");
+ nameSpan.innerHTML = displayName + meHTML;
+ }
+ else {
+ nameSpan.innerHTML = defaultLocalDisplayName;
+ }
+
+
+ nameSpan.id = 'localDisplayName';
+ this.container.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);
+
+ this.container.appendChild(editableText);
+
+ var self = this;
+ $('#localVideoContainer .displayname')
+ .bind("click", function (e) {
+
+ var editDisplayName = $('#editDisplayName');
+ e.preventDefault();
+ e.stopPropagation();
+ $('#localDisplayName').hide();
+ editDisplayName.show();
+ editDisplayName.focus();
+ editDisplayName.select();
+
+ editDisplayName.one("focusout", function (e) {
+ self.VideoLayout.inputDisplayNameHandler(this.value);
+ });
+
+ editDisplayName.on('keydown', function (e) {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ self.VideoLayout.inputDisplayNameHandler(this.value);
+ }
+ });
+ });
+ }
+};
+
+LocalVideo.prototype.inputDisplayNameHandler = function (name) {
+ NicknameHandler.setNickname(name);
+
+ var localDisplayName = $('#localDisplayName');
+ if (!localDisplayName.is(":visible")) {
+ if (NicknameHandler.getNickname()) {
+ var meHTML = APP.translation.generateTranslationHTML("me");
+ localDisplayName.html(NicknameHandler.getNickname() + " (" +
+ meHTML + ")");
+ } else {
+ var defaultHTML = APP.translation.generateTranslationHTML(
+ interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
+ localDisplayName .html(defaultHTML);
+ }
+ localDisplayName.show();
+ }
+
+ $('#editDisplayName').hide();
+};
+
+LocalVideo.prototype.createConnectionIndicator = function() {
+ if(this.connectionIndicator)
+ return;
+
+ this.connectionIndicator = new ConnectionIndicator(this, null);
+};
+
+LocalVideo.prototype.changeVideo = function (stream, isMuted) {
+ var self = this;
+
+ function localVideoClick(event) {
+ // FIXME: with Temasys plugin event arg is not an event, but
+ // the clicked object itself, so we have to skip this call
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ }
+ self.VideoLayout.handleVideoThumbClicked(
+ false,
+ APP.xmpp.myResource());
+ }
+
+ var localVideoContainerSelector = $('#localVideoContainer');
+ localVideoContainerSelector.off('click');
+ localVideoContainerSelector.on('click', localVideoClick);
+
+ // Add hover handler
+ localVideoContainerSelector.hover(
+ function() {
+ self.showDisplayName(true);
+ },
+ function() {
+ if (!LargeVideo.isLargeVideoVisible() ||
+ !LargeVideo.isCurrentlyOnLarge(self.getResourceJid())) {
+ self.showDisplayName(false);
+ }
+ }
+ );
+
+ if(isMuted) {
+ APP.UI.setVideoMute(true);
+ return;
+ }
+ this.flipX = stream.videoType != "screen";
+ var localVideo = document.createElement('video');
+ localVideo.id = 'localVideo_' +
+ APP.RTC.getStreamID(stream.getOriginalStream());
+ if (!RTCBrowserType.isIExplorer()) {
+ localVideo.autoplay = true;
+ localVideo.volume = 0; // is it required if audio is separated ?
+ }
+ localVideo.oncontextmenu = function () { return false; };
+
+ var localVideoContainer = document.getElementById('localVideoWrapper');
+ // Put the new video always in front
+ UIUtil.prependChild(localVideoContainer, localVideo);
+
+ var localVideoSelector = $('#' + localVideo.id);
+
+ // Add click handler to both video and video wrapper elements in case
+ // there's no video.
+
+ // onclick has to be used with Temasys plugin
+ localVideo.onclick = localVideoClick;
+
+ if (this.flipX) {
+ localVideoSelector.addClass("flipVideoX");
+ }
+
+ // Attach WebRTC stream
+ APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream());
+
+ // Add stream ended handler
+ stream.getOriginalStream().onended = function () {
+ // We have to re-select after attach when Temasys plugin is used,
+ // because element is replaced with
+ localVideo = $('#' + localVideo.id)[0];
+ localVideoContainer.removeChild(localVideo);
+ self.VideoLayout.updateRemovedVideo(APP.xmpp.myResource());
+ };
+};
+
+LocalVideo.prototype.joined = function (jid) {
+ this.peerJid = jid;
+};
+
+LocalVideo.prototype.getResourceJid = function () {
+ var myResource = APP.xmpp.myResource();
+ if (!myResource) {
+ console.error("Requested local resource before we're in the MUC");
+ }
+ return myResource;
+};
+
module.exports = LocalVideo;
-},{"../../RTC/RTCBrowserType":10,"../util/NicknameHandler":34,"../util/UIUtil":35,"./ConnectionIndicator":36,"./LargeVideo":37,"./SmallVideo":40}],39:[function(require,module,exports){
-/* global $, APP, require, Strophe, interfaceConfig */
-var ConnectionIndicator = require("./ConnectionIndicator");
-var SmallVideo = require("./SmallVideo");
-var AudioLevels = require("../audio_levels/AudioLevels");
-var LargeVideo = require("./LargeVideo");
-var Avatar = require("../avatar/Avatar");
-var RTCBrowserType = require("../../RTC/RTCBrowserType");
-var UIUtils = require("../util/UIUtil");
-
-function RemoteVideo(peerJid, VideoLayout) {
- this.peerJid = peerJid;
- this.resourceJid = Strophe.getResourceFromJid(peerJid);
- this.videoSpanId = 'participant_' + this.resourceJid;
- this.VideoLayout = VideoLayout;
- this.addRemoteVideoContainer();
- this.connectionIndicator = new ConnectionIndicator(
- this, this.peerJid);
- this.setDisplayName();
- var nickfield = document.createElement('span');
- nickfield.className = "nick";
- nickfield.appendChild(document.createTextNode(this.resourceJid));
- this.container.appendChild(nickfield);
- this.flipX = false;
- this.isLocal = false;
-}
-
-RemoteVideo.prototype = Object.create(SmallVideo.prototype);
-RemoteVideo.prototype.constructor = RemoteVideo;
-
-RemoteVideo.prototype.addRemoteVideoContainer = function() {
- this.container = RemoteVideo.createContainer(this.videoSpanId);
- if (APP.xmpp.isModerator())
- this.addRemoteVideoMenu();
- AudioLevels.updateAudioLevelCanvas(this.peerJid, this.VideoLayout);
-
- return this.container;
-};
-
-/**
- * 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
- */
-
-if (!interfaceConfig.filmStripOnly) {
- RemoteVideo.prototype.addRemoteVideoMenu = function () {
- var spanElement = document.createElement('span');
- spanElement.className = 'remotevideomenu';
-
- this.container.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_' + this.getResourceJid();
- spanElement.appendChild(popupmenuElement);
-
- var muteMenuItem = document.createElement('li');
- var muteLinkItem = document.createElement('a');
-
- var mutedIndicator = " ";
-
- if (!this.isMuted) {
- muteLinkItem.innerHTML = mutedIndicator +
- "
";
- muteLinkItem.className = 'mutelink';
- }
- else {
- muteLinkItem.innerHTML = mutedIndicator +
- "
";
- muteLinkItem.className = 'mutelink disabled';
- }
-
- var self = this;
- muteLinkItem.onclick = function(){
- if ($(this).attr('disabled') != undefined) {
- event.preventDefault();
- }
- var isMute = self.isMuted == true;
- APP.xmpp.setMute(self.peerJid, !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(self.peerJid);
- 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"));
- };
-
-} else {
- RemoteVideo.prototype.addRemoteVideoMenu = function() {}
-}
-
-/**
- * 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.
- */
-RemoteVideo.prototype.removeRemoteStreamElement =
- function (stream, isVideo, id) {
- if (!this.container)
- return false;
-
- var select = null;
- if (isVideo) {
- select = $('#' + id);
- }
- else
- select = $('#' + this.videoSpanId + '>audio');
-
- select.remove();
-
- console.info((isVideo ? "Video" : "Audio") +
- " removed " + this.getResourceJid(), select);
-
- if (isVideo)
- this.VideoLayout.updateRemovedVideo(this.getResourceJid());
-};
-
-/**
- * Removes RemoteVideo from the page.
- */
-RemoteVideo.prototype.remove = function () {
- console.log("Remove thumbnail", this.peerJid);
- this.removeConnectionIndicator();
- // Remove whole container
- if (this.container.parentNode)
- this.container.parentNode.removeChild(this.container);
-};
-
-RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
-
- var isVideo = stream.getVideoTracks().length > 0;
- if (!isVideo || stream.id === 'mixedmslabel') {
- return;
- }
-
- var self = this;
- var resourceJid = this.getResourceJid();
-
- // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
- // when video playback starts
- var onPlayingHandler = function () {
- // FIXME: why do i have to do this for FF?
- if (RTCBrowserType.isFirefox()) {
- APP.RTC.attachMediaStream(sel, stream);
- }
- if (RTCBrowserType.isTemasysPluginUsed()) {
- sel = self.VideoLayout.getPeerVideoSel(resourceJid);
- }
- self.VideoLayout.videoactive(sel, resourceJid);
- sel[0].onplaying = null;
- if (RTCBrowserType.isTemasysPluginUsed()) {
- // 'currentTime' is used to check if the video has started
- // and the value is not set by the plugin, so we do it
- sel[0].currentTime = 1;
- }
- };
- sel[0].onplaying = onPlayingHandler;
-};
-
-RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
- if (!this.container)
- return;
-
- var self = this;
- var isVideo = stream.getVideoTracks().length > 0;
- var streamElement = SmallVideo.createStreamElement(sid, stream);
- var newElementId = streamElement.id;
-
- // Put new stream element always in front
- UIUtils.prependChild(this.container, streamElement);
-
- var sel = $('#' + newElementId);
- sel.hide();
-
- // If the container is currently visible we attach the stream.
- if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
- this.waitForPlayback(sel, stream);
-
- APP.RTC.attachMediaStream(sel, stream);
- }
-
- stream.onended = function () {
- console.log('stream ended', this);
-
- self.removeRemoteStreamElement(stream, isVideo, newElementId);
-
- };
-
- // Name of video element name is different for IE/Safari
- var videoElem = APP.RTC.getVideoElementName();
-
- // Add click handler.
- var onClickHandler = function (event) {
-
- self.VideoLayout.handleVideoThumbClicked(false, self.getResourceJid());
-
- // On IE we need to populate this handler on video
- // and it does not give event instance as an argument,
- // so we check here for methods.
- if (event.stopPropagation && event.preventDefault) {
- event.stopPropagation();
- event.preventDefault();
- }
- return false;
- };
- this.container.onclick = onClickHandler;
- // reselect
- if (RTCBrowserType.isTemasysPluginUsed())
- sel = $('#' + newElementId);
- sel[0].onclick = onClickHandler;
-
- //FIXME
- // Add hover handler
- $(this.container).hover(
- function() {
- self.showDisplayName(true);
- },
- function() {
- // If the video has been "pinned" by the user we want to
- // keep the display name on place.
- if (!LargeVideo.isLargeVideoVisible() ||
- !LargeVideo.isCurrentlyOnLarge(self.getResourceJid()))
- self.showDisplayName(false);
- }
- );
-},
-
-/**
- * Show/hide peer container for the given resourceJid.
- */
-RemoteVideo.prototype.showPeerContainer = function (state) {
- if (!this.container)
- return;
-
- var isHide = state === 'hide';
- var resizeThumbnails = false;
-
- if (!isHide) {
- if (!$(this.container).is(':visible')) {
- resizeThumbnails = true;
- $(this.container).show();
- }
-
- this.showAvatar(state !== 'show');
- }
- else if ($(this.container).is(':visible') && isHide)
- {
- resizeThumbnails = true;
- $(this.container).hide();
- if(this.connectionIndicator)
- this.connectionIndicator.hide();
- }
-
- if (resizeThumbnails) {
- this.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);
-
-};
-
-RemoteVideo.prototype.removeConnectionIndicator = function () {
- if (this.connectionIndicator)
- this.connectionIndicator.remove();
-};
-
-RemoteVideo.prototype.hideConnectionIndicator = function () {
- if (this.connectionIndicator)
- this.connectionIndicator.hide();
-};
-
-/**
- * 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
- */
-RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
- var muteMenuItem
- = $('#remote_popupmenu_' + this.getResourceJid() + '>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';
- }
- }
-};
-
-/**
- * Sets the display name for the given video span id.
- */
-RemoteVideo.prototype.setDisplayName = function(displayName, key) {
-
- if (!this.container) {
- console.warn( "Unable to set displayName - " + this.videoSpanId +
- " does not exist");
- return;
- }
-
- var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
-
- // If we already have a display name for this video.
- if (nameSpan.length > 0) {
- if (displayName && displayName.length > 0) {
- $('#' + this.videoSpanId + '_name').html(displayName);
- }
- else if (key && key.length > 0) {
- var nameHtml = APP.translation.generateTranslationHTML(key);
- $('#' + this.videoSpanId + '_name').html(nameHtml);
- }
- else
- $('#' + this.videoSpanId + '_name').text(
- interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
- } else {
- nameSpan = document.createElement('span');
- nameSpan.className = 'displayname';
- $('#' + this.videoSpanId)[0].appendChild(nameSpan);
-
-
- if (displayName && displayName.length > 0) {
- nameSpan.innerText = displayName;
- }
- else
- nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
-
- nameSpan.id = this.videoSpanId + '_name';
- }
-};
-
-/**
- * Removes remote video menu element from video element identified by
- * given videoElementId .
- *
- * @param videoElementId the id of local or remote video element.
- */
-RemoteVideo.prototype.removeRemoteVideoMenu = function() {
- var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu');
- if (menuSpan.length) {
- menuSpan.remove();
- }
-};
-
-RemoteVideo.prototype.getResourceJid = function () {
- if (!this.resourceJid) {
- console.error("Undefined resource jid");
- }
- return this.resourceJid;
-};
-
-RemoteVideo.createContainer = function (spanId) {
- var container = document.createElement('span');
- container.id = spanId;
- container.className = 'videocontainer';
- var remotes = document.getElementById('remoteVideos');
- return remotes.appendChild(container);
-};
-
-
+},{"../../RTC/RTCBrowserType":8,"../util/NicknameHandler":32,"../util/UIUtil":33,"./ConnectionIndicator":34,"./LargeVideo":35,"./SmallVideo":38}],37:[function(require,module,exports){
+/* global $, APP, require, Strophe, interfaceConfig */
+var ConnectionIndicator = require("./ConnectionIndicator");
+var SmallVideo = require("./SmallVideo");
+var AudioLevels = require("../audio_levels/AudioLevels");
+var LargeVideo = require("./LargeVideo");
+var Avatar = require("../avatar/Avatar");
+var RTCBrowserType = require("../../RTC/RTCBrowserType");
+var UIUtils = require("../util/UIUtil");
+
+function RemoteVideo(peerJid, VideoLayout) {
+ this.peerJid = peerJid;
+ this.resourceJid = Strophe.getResourceFromJid(peerJid);
+ this.videoSpanId = 'participant_' + this.resourceJid;
+ this.VideoLayout = VideoLayout;
+ this.addRemoteVideoContainer();
+ this.connectionIndicator = new ConnectionIndicator(
+ this, this.peerJid);
+ this.setDisplayName();
+ var nickfield = document.createElement('span');
+ nickfield.className = "nick";
+ nickfield.appendChild(document.createTextNode(this.resourceJid));
+ this.container.appendChild(nickfield);
+ this.flipX = false;
+ this.isLocal = false;
+}
+
+RemoteVideo.prototype = Object.create(SmallVideo.prototype);
+RemoteVideo.prototype.constructor = RemoteVideo;
+
+RemoteVideo.prototype.addRemoteVideoContainer = function() {
+ this.container = RemoteVideo.createContainer(this.videoSpanId);
+ if (APP.xmpp.isModerator())
+ this.addRemoteVideoMenu();
+ AudioLevels.updateAudioLevelCanvas(this.peerJid, this.VideoLayout);
+
+ return this.container;
+};
+
+/**
+ * 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
+ */
+
+if (!interfaceConfig.filmStripOnly) {
+ RemoteVideo.prototype.addRemoteVideoMenu = function () {
+ var spanElement = document.createElement('span');
+ spanElement.className = 'remotevideomenu';
+
+ this.container.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_' + this.getResourceJid();
+ spanElement.appendChild(popupmenuElement);
+
+ var muteMenuItem = document.createElement('li');
+ var muteLinkItem = document.createElement('a');
+
+ var mutedIndicator = " ";
+
+ if (!this.isMuted) {
+ muteLinkItem.innerHTML = mutedIndicator +
+ "
";
+ muteLinkItem.className = 'mutelink';
+ }
+ else {
+ muteLinkItem.innerHTML = mutedIndicator +
+ "
";
+ muteLinkItem.className = 'mutelink disabled';
+ }
+
+ var self = this;
+ muteLinkItem.onclick = function(){
+ if ($(this).attr('disabled') != undefined) {
+ event.preventDefault();
+ }
+ var isMute = self.isMuted == true;
+ APP.xmpp.setMute(self.peerJid, !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(self.peerJid);
+ 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"));
+ };
+
+} else {
+ RemoteVideo.prototype.addRemoteVideoMenu = function() {}
+}
+
+/**
+ * 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.
+ */
+RemoteVideo.prototype.removeRemoteStreamElement =
+ function (stream, isVideo, id) {
+ if (!this.container)
+ return false;
+
+ var select = null;
+ if (isVideo) {
+ select = $('#' + id);
+ }
+ else
+ select = $('#' + this.videoSpanId + '>audio');
+
+ select.remove();
+
+ console.info((isVideo ? "Video" : "Audio") +
+ " removed " + this.getResourceJid(), select);
+
+ if (isVideo)
+ this.VideoLayout.updateRemovedVideo(this.getResourceJid());
+};
+
+/**
+ * Removes RemoteVideo from the page.
+ */
+RemoteVideo.prototype.remove = function () {
+ console.log("Remove thumbnail", this.peerJid);
+ this.removeConnectionIndicator();
+ // Remove whole container
+ if (this.container.parentNode)
+ this.container.parentNode.removeChild(this.container);
+};
+
+RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
+
+ var isVideo = stream.getVideoTracks().length > 0;
+ if (!isVideo || stream.id === 'mixedmslabel') {
+ return;
+ }
+
+ var self = this;
+ var resourceJid = this.getResourceJid();
+
+ // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
+ // when video playback starts
+ var onPlayingHandler = function () {
+ // FIXME: why do i have to do this for FF?
+ if (RTCBrowserType.isFirefox()) {
+ APP.RTC.attachMediaStream(sel, stream);
+ }
+ if (RTCBrowserType.isTemasysPluginUsed()) {
+ sel = self.VideoLayout.getPeerVideoSel(resourceJid);
+ }
+ self.VideoLayout.videoactive(sel, resourceJid);
+ sel[0].onplaying = null;
+ if (RTCBrowserType.isTemasysPluginUsed()) {
+ // 'currentTime' is used to check if the video has started
+ // and the value is not set by the plugin, so we do it
+ sel[0].currentTime = 1;
+ }
+ };
+ sel[0].onplaying = onPlayingHandler;
+};
+
+RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
+ if (!this.container)
+ return;
+
+ var self = this;
+ var isVideo = stream.getVideoTracks().length > 0;
+ var streamElement = SmallVideo.createStreamElement(sid, stream);
+ var newElementId = streamElement.id;
+
+ // Put new stream element always in front
+ UIUtils.prependChild(this.container, streamElement);
+
+ var sel = $('#' + newElementId);
+ sel.hide();
+
+ // If the container is currently visible we attach the stream.
+ if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
+ this.waitForPlayback(sel, stream);
+
+ APP.RTC.attachMediaStream(sel, stream);
+ }
+
+ stream.onended = function () {
+ console.log('stream ended', this);
+
+ self.removeRemoteStreamElement(stream, isVideo, newElementId);
+
+ };
+
+ // Name of video element name is different for IE/Safari
+ var videoElem = APP.RTC.getVideoElementName();
+
+ // Add click handler.
+ var onClickHandler = function (event) {
+
+ self.VideoLayout.handleVideoThumbClicked(false, self.getResourceJid());
+
+ // On IE we need to populate this handler on video
+ // and it does not give event instance as an argument,
+ // so we check here for methods.
+ if (event.stopPropagation && event.preventDefault) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ return false;
+ };
+ this.container.onclick = onClickHandler;
+ // reselect
+ if (RTCBrowserType.isTemasysPluginUsed())
+ sel = $('#' + newElementId);
+ sel[0].onclick = onClickHandler;
+
+ //FIXME
+ // Add hover handler
+ $(this.container).hover(
+ function() {
+ self.showDisplayName(true);
+ },
+ function() {
+ // If the video has been "pinned" by the user we want to
+ // keep the display name on place.
+ if (!LargeVideo.isLargeVideoVisible() ||
+ !LargeVideo.isCurrentlyOnLarge(self.getResourceJid()))
+ self.showDisplayName(false);
+ }
+ );
+},
+
+/**
+ * Show/hide peer container for the given resourceJid.
+ */
+RemoteVideo.prototype.showPeerContainer = function (state) {
+ if (!this.container)
+ return;
+
+ var isHide = state === 'hide';
+ var resizeThumbnails = false;
+
+ if (!isHide) {
+ if (!$(this.container).is(':visible')) {
+ resizeThumbnails = true;
+ $(this.container).show();
+ }
+
+ this.showAvatar(state !== 'show');
+ }
+ else if ($(this.container).is(':visible') && isHide)
+ {
+ resizeThumbnails = true;
+ $(this.container).hide();
+ if(this.connectionIndicator)
+ this.connectionIndicator.hide();
+ }
+
+ if (resizeThumbnails) {
+ this.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);
+
+};
+
+RemoteVideo.prototype.removeConnectionIndicator = function () {
+ if (this.connectionIndicator)
+ this.connectionIndicator.remove();
+};
+
+RemoteVideo.prototype.hideConnectionIndicator = function () {
+ if (this.connectionIndicator)
+ this.connectionIndicator.hide();
+};
+
+/**
+ * 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
+ */
+RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
+ var muteMenuItem
+ = $('#remote_popupmenu_' + this.getResourceJid() + '>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';
+ }
+ }
+};
+
+/**
+ * Sets the display name for the given video span id.
+ */
+RemoteVideo.prototype.setDisplayName = function(displayName, key) {
+
+ if (!this.container) {
+ console.warn( "Unable to set displayName - " + this.videoSpanId +
+ " does not exist");
+ return;
+ }
+
+ var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
+
+ // If we already have a display name for this video.
+ if (nameSpan.length > 0) {
+ if (displayName && displayName.length > 0) {
+ $('#' + this.videoSpanId + '_name').html(displayName);
+ }
+ else if (key && key.length > 0) {
+ var nameHtml = APP.translation.generateTranslationHTML(key);
+ $('#' + this.videoSpanId + '_name').html(nameHtml);
+ }
+ else
+ $('#' + this.videoSpanId + '_name').text(
+ interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
+ } else {
+ nameSpan = document.createElement('span');
+ nameSpan.className = 'displayname';
+ $('#' + this.videoSpanId)[0].appendChild(nameSpan);
+
+
+ if (displayName && displayName.length > 0) {
+ nameSpan.innerText = displayName;
+ }
+ else
+ nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
+
+ nameSpan.id = this.videoSpanId + '_name';
+ }
+};
+
+/**
+ * Removes remote video menu element from video element identified by
+ * given videoElementId .
+ *
+ * @param videoElementId the id of local or remote video element.
+ */
+RemoteVideo.prototype.removeRemoteVideoMenu = function() {
+ var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu');
+ if (menuSpan.length) {
+ menuSpan.remove();
+ }
+};
+
+RemoteVideo.prototype.getResourceJid = function () {
+ if (!this.resourceJid) {
+ console.error("Undefined resource jid");
+ }
+ return this.resourceJid;
+};
+
+RemoteVideo.createContainer = function (spanId) {
+ var container = document.createElement('span');
+ container.id = spanId;
+ container.className = 'videocontainer';
+ var remotes = document.getElementById('remoteVideos');
+ return remotes.appendChild(container);
+};
+
+
module.exports = RemoteVideo;
-},{"../../RTC/RTCBrowserType":10,"../audio_levels/AudioLevels":14,"../avatar/Avatar":18,"../util/UIUtil":35,"./ConnectionIndicator":36,"./LargeVideo":37,"./SmallVideo":40}],40:[function(require,module,exports){
-/* global $, APP, require */
-var Avatar = require("../avatar/Avatar");
-var UIUtil = require("../util/UIUtil");
-var LargeVideo = require("./LargeVideo");
-var RTCBrowserType = require("../../RTC/RTCBrowserType");
-
-function SmallVideo() {
- this.isMuted = false;
- this.hasAvatar = false;
-}
-
-function setVisibility(selector, show) {
- if (selector && selector.length > 0) {
- selector.css("visibility", show ? "visible" : "hidden");
- }
-}
-
-SmallVideo.prototype.showDisplayName = function(isShow) {
- var nameSpan = $('#' + this.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;");
- }
-};
-
-SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
- if(!this.container)
- return;
-
- var noMic = $("#" + this.videoSpanId + " > .noMic");
- var noVideo = $("#" + this.videoSpanId + " > .noVideo");
-
- noMic.remove();
- noVideo.remove();
- if (!devices.audio) {
- this.container.appendChild(
- document.createElement("div")).setAttribute("class", "noMic");
- }
-
- if (!devices.video) {
- this.container.appendChild(
- document.createElement("div")).setAttribute("class", "noVideo");
- }
-
- if (!devices.audio && !devices.video) {
- noMic.css("background-position", "75%");
- noVideo.css("background-position", "25%");
- noVideo.css("background-color", "transparent");
- }
-};
-
-/**
- * Sets the type of the video displayed by this instance.
- * @param videoType 'camera' or 'screen'
- */
-SmallVideo.prototype.setVideoType = function (videoType) {
- this.videoType = videoType;
-};
-
-/**
- * Returns the type of the video displayed by this instance.
- * @returns {String} 'camera', 'screen' or undefined.
- */
-SmallVideo.prototype.getVideoType = function () {
- return this.videoType;
-};
-
-/**
- * Shows the presence status message for the given video.
- */
-SmallVideo.prototype.setPresenceStatus = function (statusMsg) {
- if (!this.container) {
- // No container
- return;
- }
-
- var statusSpan = $('#' + this.videoSpanId + '>span.status');
- if (!statusSpan.length) {
- //Add status span
- statusSpan = document.createElement('span');
- statusSpan.className = 'status';
- statusSpan.id = this.videoSpanId + '_status';
- $('#' + this.videoSpanId)[0].appendChild(statusSpan);
-
- statusSpan = $('#' + this.videoSpanId + '>span.status');
- }
-
- // Display status
- if (statusMsg && statusMsg.length) {
- $('#' + this.videoSpanId + '_status').text(statusMsg);
- statusSpan.get(0).setAttribute("style", "display:inline-block;");
- }
- else {
- // Hide
- statusSpan.get(0).setAttribute("style", "display:none;");
- }
-};
-
-/**
- * Creates an audio or video stream element.
- */
-SmallVideo.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;
- if (!RTCBrowserType.isIExplorer()) {
- element.autoplay = true;
- }
- element.oncontextmenu = function () { return false; };
-
- return element;
-};
-
-/**
- * Updates the data for the indicator
- * @param id the id of the indicator
- * @param percent the percent for connection quality
- * @param object the data
- */
-SmallVideo.prototype.updateStatsIndicator = function (percent, object) {
- if(this.connectionIndicator)
- this.connectionIndicator.updateConnectionQuality(percent, object);
-};
-
-SmallVideo.prototype.hideIndicator = function () {
- if(this.connectionIndicator)
- this.connectionIndicator.hideIndicator();
-};
-
-
-/**
- * Shows audio muted indicator over small videos.
- * @param {string} isMuted
- */
-SmallVideo.prototype.showAudioIndicator = function(isMuted) {
- var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
-
- if (!isMuted) {
- 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");
-
- this.container.appendChild(audioMutedSpan);
- APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
- var mutedIndicator = document.createElement('i');
- mutedIndicator.className = 'icon-mic-disabled';
- audioMutedSpan.appendChild(mutedIndicator);
-
- }
- this.updateIconPositions();
- }
- this.isMuted = isMuted;
-};
-
-/**
- * Shows video muted indicator over small videos.
- */
-SmallVideo.prototype.showVideoIndicator = function(isMuted) {
- this.showAvatar(isMuted);
-
- var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
-
- if (isMuted === false) {
- if (videoMutedSpan.length > 0) {
- videoMutedSpan.remove();
- }
- }
- else {
- if(videoMutedSpan.length == 0) {
- videoMutedSpan = document.createElement('span');
- videoMutedSpan.className = 'videoMuted';
-
- this.container.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($('#' + this.videoSpanId + " > span > i"));
- }
-
- this.updateIconPositions();
-
- }
-};
-
-SmallVideo.prototype.enableDominantSpeaker = function (isEnable) {
- var resourceJid = this.getResourceJid();
- var displayName = resourceJid;
- var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
- if (nameSpan.length > 0)
- displayName = nameSpan.html();
-
- console.log("UI enable dominant speaker",
- displayName,
- resourceJid,
- isEnable);
-
-
- if (!this.container) {
- return;
- }
-
- if (isEnable) {
- this.showDisplayName(LargeVideo.getState() === "video");
-
- if (!this.container.classList.contains("dominantspeaker"))
- this.container.classList.add("dominantspeaker");
- }
- else {
- this.showDisplayName(false);
-
- if (this.container.classList.contains("dominantspeaker"))
- this.container.classList.remove("dominantspeaker");
- }
-
- this.showAvatar();
-};
-
-SmallVideo.prototype.updateIconPositions = function () {
- var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
- var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
- var videoMutedSpan = $('#' + this.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"});
- }
-};
-
-/**
- * Creates the element indicating the moderator(owner) of the conference.
- *
- * @param parentElement the parent element where the owner indicator will
- * be added
- */
-SmallVideo.prototype.createModeratorIndicatorElement = function () {
- // Show moderator indicator
- var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
-
- if (!indicatorSpan || indicatorSpan.length === 0) {
- indicatorSpan = document.createElement('span');
- indicatorSpan.className = 'focusindicator';
-
- this.container.appendChild(indicatorSpan);
- indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
- }
-
- if (indicatorSpan.children().length !== 0)
- return;
- var moderatorIndicator = document.createElement('i');
- moderatorIndicator.className = 'fa fa-star';
- indicatorSpan[0].appendChild(moderatorIndicator);
-
- UIUtil.setTooltip(indicatorSpan[0],
- "videothumbnail.moderator",
- "top");
-
- //translates text in focus indicators
- APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator'));
-};
-
-SmallVideo.prototype.getSrc = function () {
- var videoElement = APP.RTC.getVideoElementName();
- return APP.RTC.getVideoSrc(
- $('#' + this.videoSpanId).find(videoElement).get(0));
-};
-
-SmallVideo.prototype.focus = function(isFocused) {
- if(!isFocused) {
- this.container.classList.remove("videoContainerFocused");
- } else {
- this.container.classList.add("videoContainerFocused");
- }
-};
-
-SmallVideo.prototype.hasVideo = function () {
- return $("#" + this.videoSpanId).find(
- APP.RTC.getVideoElementName()).length !== 0;
-};
-
-/**
- * Hides or shows the user's avatar
- * @param show whether we should show the avatar or not
- * video because there is no dominant speaker and no focused speaker
- */
-SmallVideo.prototype.showAvatar = function (show) {
- if (!this.hasAvatar) {
- if (this.peerJid) {
- // Init avatar
- this.avatarChanged(Avatar.getThumbUrl(this.peerJid));
- } else {
- console.error("Unable to init avatar - no peerjid", this);
- return;
- }
- }
-
- var resourceJid = this.getResourceJid();
- var videoElem = APP.RTC.getVideoElementName();
- var video = $('#' + this.videoSpanId).find(videoElem);
- var avatar = $('#avatar_' + resourceJid);
-
- if (show === undefined || show === null) {
- if (!this.isLocal &&
- !this.VideoLayout.isInLastN(resourceJid)) {
- show = true;
- } else {
- // We want to show the avatar when the video is muted or not exists
- // that is when 'true' or 'null' is returned
- show = APP.RTC.isVideoMuted(this.peerJid) !== false;
- }
- }
-
- if (LargeVideo.showAvatar(resourceJid, show)) {
- setVisibility(avatar, false);
- setVisibility(video, false);
- } else {
- if (video && video.length > 0) {
- setVisibility(video, !show);
- }
- setVisibility(avatar, show);
- }
-};
-
-SmallVideo.prototype.avatarChanged = function (thumbUrl) {
- var thumbnail = $('#' + this.videoSpanId);
- var resourceJid = this.getResourceJid();
- var avatar = $('#avatar_' + resourceJid);
- this.hasAvatar = true;
-
- // set the avatar in the thumbnail
- if (avatar && avatar.length > 0) {
- avatar[0].src = thumbUrl;
- } else {
- if (thumbnail && thumbnail.length > 0) {
- avatar = document.createElement('img');
- avatar.id = 'avatar_' + resourceJid;
- avatar.className = 'userAvatar';
- avatar.src = thumbUrl;
- thumbnail.append(avatar);
- }
- }
-};
-
+},{"../../RTC/RTCBrowserType":8,"../audio_levels/AudioLevels":12,"../avatar/Avatar":16,"../util/UIUtil":33,"./ConnectionIndicator":34,"./LargeVideo":35,"./SmallVideo":38}],38:[function(require,module,exports){
+/* global $, APP, require */
+var Avatar = require("../avatar/Avatar");
+var UIUtil = require("../util/UIUtil");
+var LargeVideo = require("./LargeVideo");
+var RTCBrowserType = require("../../RTC/RTCBrowserType");
+
+function SmallVideo() {
+ this.isMuted = false;
+ this.hasAvatar = false;
+}
+
+function setVisibility(selector, show) {
+ if (selector && selector.length > 0) {
+ selector.css("visibility", show ? "visible" : "hidden");
+ }
+}
+
+SmallVideo.prototype.showDisplayName = function(isShow) {
+ var nameSpan = $('#' + this.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;");
+ }
+};
+
+SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
+ if(!this.container)
+ return;
+
+ var noMic = $("#" + this.videoSpanId + " > .noMic");
+ var noVideo = $("#" + this.videoSpanId + " > .noVideo");
+
+ noMic.remove();
+ noVideo.remove();
+ if (!devices.audio) {
+ this.container.appendChild(
+ document.createElement("div")).setAttribute("class", "noMic");
+ }
+
+ if (!devices.video) {
+ this.container.appendChild(
+ document.createElement("div")).setAttribute("class", "noVideo");
+ }
+
+ if (!devices.audio && !devices.video) {
+ noMic.css("background-position", "75%");
+ noVideo.css("background-position", "25%");
+ noVideo.css("background-color", "transparent");
+ }
+};
+
+/**
+ * Sets the type of the video displayed by this instance.
+ * @param videoType 'camera' or 'screen'
+ */
+SmallVideo.prototype.setVideoType = function (videoType) {
+ this.videoType = videoType;
+};
+
+/**
+ * Returns the type of the video displayed by this instance.
+ * @returns {String} 'camera', 'screen' or undefined.
+ */
+SmallVideo.prototype.getVideoType = function () {
+ return this.videoType;
+};
+
+/**
+ * Shows the presence status message for the given video.
+ */
+SmallVideo.prototype.setPresenceStatus = function (statusMsg) {
+ if (!this.container) {
+ // No container
+ return;
+ }
+
+ var statusSpan = $('#' + this.videoSpanId + '>span.status');
+ if (!statusSpan.length) {
+ //Add status span
+ statusSpan = document.createElement('span');
+ statusSpan.className = 'status';
+ statusSpan.id = this.videoSpanId + '_status';
+ $('#' + this.videoSpanId)[0].appendChild(statusSpan);
+
+ statusSpan = $('#' + this.videoSpanId + '>span.status');
+ }
+
+ // Display status
+ if (statusMsg && statusMsg.length) {
+ $('#' + this.videoSpanId + '_status').text(statusMsg);
+ statusSpan.get(0).setAttribute("style", "display:inline-block;");
+ }
+ else {
+ // Hide
+ statusSpan.get(0).setAttribute("style", "display:none;");
+ }
+};
+
+/**
+ * Creates an audio or video stream element.
+ */
+SmallVideo.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;
+ if (!RTCBrowserType.isIExplorer()) {
+ element.autoplay = true;
+ }
+ element.oncontextmenu = function () { return false; };
+
+ return element;
+};
+
+/**
+ * Updates the data for the indicator
+ * @param id the id of the indicator
+ * @param percent the percent for connection quality
+ * @param object the data
+ */
+SmallVideo.prototype.updateStatsIndicator = function (percent, object) {
+ if(this.connectionIndicator)
+ this.connectionIndicator.updateConnectionQuality(percent, object);
+};
+
+SmallVideo.prototype.hideIndicator = function () {
+ if(this.connectionIndicator)
+ this.connectionIndicator.hideIndicator();
+};
+
+
+/**
+ * Shows audio muted indicator over small videos.
+ * @param {string} isMuted
+ */
+SmallVideo.prototype.showAudioIndicator = function(isMuted) {
+ var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
+
+ if (!isMuted) {
+ 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");
+
+ this.container.appendChild(audioMutedSpan);
+ APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
+ var mutedIndicator = document.createElement('i');
+ mutedIndicator.className = 'icon-mic-disabled';
+ audioMutedSpan.appendChild(mutedIndicator);
+
+ }
+ this.updateIconPositions();
+ }
+ this.isMuted = isMuted;
+};
+
+/**
+ * Shows video muted indicator over small videos.
+ */
+SmallVideo.prototype.showVideoIndicator = function(isMuted) {
+ this.showAvatar(isMuted);
+
+ var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
+
+ if (isMuted === false) {
+ if (videoMutedSpan.length > 0) {
+ videoMutedSpan.remove();
+ }
+ }
+ else {
+ if(videoMutedSpan.length == 0) {
+ videoMutedSpan = document.createElement('span');
+ videoMutedSpan.className = 'videoMuted';
+
+ this.container.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($('#' + this.videoSpanId + " > span > i"));
+ }
+
+ this.updateIconPositions();
+
+ }
+};
+
+SmallVideo.prototype.enableDominantSpeaker = function (isEnable) {
+ var resourceJid = this.getResourceJid();
+ var displayName = resourceJid;
+ var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
+ if (nameSpan.length > 0)
+ displayName = nameSpan.html();
+
+ console.log("UI enable dominant speaker",
+ displayName,
+ resourceJid,
+ isEnable);
+
+
+ if (!this.container) {
+ return;
+ }
+
+ if (isEnable) {
+ this.showDisplayName(LargeVideo.getState() === "video");
+
+ if (!this.container.classList.contains("dominantspeaker"))
+ this.container.classList.add("dominantspeaker");
+ }
+ else {
+ this.showDisplayName(false);
+
+ if (this.container.classList.contains("dominantspeaker"))
+ this.container.classList.remove("dominantspeaker");
+ }
+
+ this.showAvatar();
+};
+
+SmallVideo.prototype.updateIconPositions = function () {
+ var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
+ var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
+ var videoMutedSpan = $('#' + this.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"});
+ }
+};
+
+/**
+ * Creates the element indicating the moderator(owner) of the conference.
+ *
+ * @param parentElement the parent element where the owner indicator will
+ * be added
+ */
+SmallVideo.prototype.createModeratorIndicatorElement = function () {
+ // Show moderator indicator
+ var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
+
+ if (!indicatorSpan || indicatorSpan.length === 0) {
+ indicatorSpan = document.createElement('span');
+ indicatorSpan.className = 'focusindicator';
+
+ this.container.appendChild(indicatorSpan);
+ indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
+ }
+
+ if (indicatorSpan.children().length !== 0)
+ return;
+ var moderatorIndicator = document.createElement('i');
+ moderatorIndicator.className = 'fa fa-star';
+ indicatorSpan[0].appendChild(moderatorIndicator);
+
+ UIUtil.setTooltip(indicatorSpan[0],
+ "videothumbnail.moderator",
+ "top");
+
+ //translates text in focus indicators
+ APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator'));
+};
+
+SmallVideo.prototype.getSrc = function () {
+ var videoElement = APP.RTC.getVideoElementName();
+ return APP.RTC.getVideoSrc(
+ $('#' + this.videoSpanId).find(videoElement).get(0));
+};
+
+SmallVideo.prototype.focus = function(isFocused) {
+ if(!isFocused) {
+ this.container.classList.remove("videoContainerFocused");
+ } else {
+ this.container.classList.add("videoContainerFocused");
+ }
+};
+
+SmallVideo.prototype.hasVideo = function () {
+ return $("#" + this.videoSpanId).find(
+ APP.RTC.getVideoElementName()).length !== 0;
+};
+
+/**
+ * Hides or shows the user's avatar
+ * @param show whether we should show the avatar or not
+ * video because there is no dominant speaker and no focused speaker
+ */
+SmallVideo.prototype.showAvatar = function (show) {
+ if (!this.hasAvatar) {
+ if (this.peerJid) {
+ // Init avatar
+ this.avatarChanged(Avatar.getThumbUrl(this.peerJid));
+ } else {
+ console.error("Unable to init avatar - no peerjid", this);
+ return;
+ }
+ }
+
+ var resourceJid = this.getResourceJid();
+ var videoElem = APP.RTC.getVideoElementName();
+ var video = $('#' + this.videoSpanId).find(videoElem);
+ var avatar = $('#avatar_' + resourceJid);
+
+ if (show === undefined || show === null) {
+ if (!this.isLocal &&
+ !this.VideoLayout.isInLastN(resourceJid)) {
+ show = true;
+ } else {
+ // We want to show the avatar when the video is muted or not exists
+ // that is when 'true' or 'null' is returned
+ show = APP.RTC.isVideoMuted(this.peerJid) !== false;
+ }
+ }
+
+ if (LargeVideo.showAvatar(resourceJid, show)) {
+ setVisibility(avatar, false);
+ setVisibility(video, false);
+ } else {
+ if (video && video.length > 0) {
+ setVisibility(video, !show);
+ }
+ setVisibility(avatar, show);
+ }
+};
+
+SmallVideo.prototype.avatarChanged = function (thumbUrl) {
+ var thumbnail = $('#' + this.videoSpanId);
+ var resourceJid = this.getResourceJid();
+ var avatar = $('#avatar_' + resourceJid);
+ this.hasAvatar = true;
+
+ // set the avatar in the thumbnail
+ if (avatar && avatar.length > 0) {
+ avatar[0].src = thumbUrl;
+ } else {
+ if (thumbnail && thumbnail.length > 0) {
+ avatar = document.createElement('img');
+ avatar.id = 'avatar_' + resourceJid;
+ avatar.className = 'userAvatar';
+ avatar.src = thumbUrl;
+ thumbnail.append(avatar);
+ }
+ }
+};
+
module.exports = SmallVideo;
-},{"../../RTC/RTCBrowserType":10,"../avatar/Avatar":18,"../util/UIUtil":35,"./LargeVideo":37}],41:[function(require,module,exports){
-/* global config, APP, $, Strophe, require, interfaceConfig */
-var AudioLevels = require("../audio_levels/AudioLevels");
-var ContactList = require("../side_pannels/contactlist/ContactList");
-var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
-var UIEvents = require("../../../service/UI/UIEvents");
-var UIUtil = require("../util/UIUtil");
-
-var RTC = require("../../RTC/RTC");
-var RTCBrowserType = require('../../RTC/RTCBrowserType');
-
-var RemoteVideo = require("./RemoteVideo");
-var LargeVideo = require("./LargeVideo");
-var LocalVideo = require("./LocalVideo");
-
-
-var remoteVideos = {};
-var remoteVideoTypes = {};
-var localVideoThumbnail = null;
-
-var currentDominantSpeaker = null;
-var lastNCount = config.channelLastN;
-var localLastNCount = config.channelLastN;
-var localLastNSet = [];
-var lastNEndpointsCache = [];
-var lastNPickupJid = null;
-
-var eventEmitter = null;
-
-/**
- * Currently focused video jid
- * @type {String}
- */
-var focusedVideoResourceJid = null;
-
-var VideoLayout = (function (my) {
- my.init = function (emitter) {
- eventEmitter = emitter;
- localVideoThumbnail = new LocalVideo(VideoLayout);
- if (interfaceConfig.filmStripOnly) {
- LargeVideo.disable();
- } else {
- LargeVideo.init(VideoLayout, emitter);
- }
-
- VideoLayout.resizeLargeVideoContainer();
-
- };
-
- my.isInLastN = function(resource) {
- return lastNCount < 0 || // lastN is disabled
- // lastNEndpoints cache not built yet
- (lastNCount > 0 && lastNEndpointsCache.length == 0) ||
- (lastNEndpointsCache &&
- lastNEndpointsCache.indexOf(resource) !== -1);
- };
-
- my.changeLocalStream = function (stream, isMuted) {
- VideoLayout.changeLocalVideo(stream, isMuted);
- };
-
- my.changeLocalAudio = function(stream, isMuted) {
- if (isMuted)
- APP.UI.setAudioMuted(true, true);
- APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
- var localAudio = document.getElementById('localAudio');
- // Writing volume not allowed in IE
- if (!RTCBrowserType.isIExplorer()) {
- localAudio.autoplay = true;
- localAudio.volume = 0;
- }
- };
-
- my.changeLocalVideo = function(stream, isMuted) {
- // Set default display name.
- localVideoThumbnail.setDisplayName();
- localVideoThumbnail.createConnectionIndicator();
-
- this.onVideoTypeChanged(APP.xmpp.myResource(), stream.videoType);
-
- AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
-
- localVideoThumbnail.changeVideo(stream, isMuted);
-
- /* force update if we're currently being displayed */
- if (LargeVideo.isCurrentlyOnLarge(APP.xmpp.myResource())) {
- LargeVideo.updateLargeVideo(APP.xmpp.myResource(), true);
- }
- };
-
- my.mucJoined = function () {
- var myResourceJid = APP.xmpp.myResource();
- localVideoThumbnail.joined(APP.xmpp.myJid());
-
- if (!LargeVideo.getResourceJid())
- LargeVideo.updateLargeVideo(myResourceJid, true);
- };
-
- /**
- * Adds or removes icons for not available camera and microphone.
- * @param resourceJid the jid of user
- * @param devices available devices
- */
- my.setDeviceAvailabilityIcons = function (resourceJid, devices) {
- if(!devices)
- return;
-
- if(!resourceJid) {
- localVideoThumbnail.setDeviceAvailabilityIcons(devices);
- } else {
- if(remoteVideos[resourceJid])
- remoteVideos[resourceJid].setDeviceAvailabilityIcons(devices);
- }
- };
-
- /**
- * 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(resourceJid) {
-
- if (resourceJid === LargeVideo.getResourceJid()) {
- var newResourceJid;
- // We'll show user's avatar if he is the dominant speaker or if
- // his video thumbnail is pinned
- if (remoteVideos[resourceJid] &&
- resourceJid === focusedVideoResourceJid ||
- resourceJid === currentDominantSpeaker) {
- newResourceJid = resourceJid;
- } else {
- // Otherwise select last visible video
- newResourceJid = this.electLastVisibleVideo();
- }
- LargeVideo.updateLargeVideo(newResourceJid);
- }
- };
-
- my.electLastVisibleVideo = function() {
- // pick the last visible video in the row
- // if nobody else is left, this picks the local video
- var jid;
- var videoElem = RTC.getVideoElementName();
- var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>' + videoElem);
- if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
- jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
- } else {
- console.info("Last visible video no longer exists");
- pick = $('#remoteVideos>span[id!="mixedstream"]>' + videoElem);
- if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
- jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
- } else {
- // Try local video
- console.info("Fallback to local video...");
- jid = APP.xmpp.myResource();
- }
- }
- console.info("electLastVisibleVideo: " + jid);
- return jid;
- };
-
- my.onRemoteStreamAdded = function (stream) {
- if (stream.peerjid) {
- VideoLayout.ensurePeerContainerExists(stream.peerjid);
-
- var resourceJid = Strophe.getResourceFromJid(stream.peerjid);
- remoteVideos[resourceJid].addRemoteStreamElement(
- stream.sid,
- stream.getOriginalStream(),
- stream.ssrc);
- }
- };
-
- my.getLargeVideoResource = function () {
- return LargeVideo.getResourceJid();
- };
-
- /**
- * Called when large video update is finished
- * @param currentSmallVideo small video currently displayed on large video
- */
- my.largeVideoUpdated = function (currentSmallVideo) {
- // Makes sure that dominant speaker UI
- // is enabled only on current small video
- localVideoThumbnail.enableDominantSpeaker(
- localVideoThumbnail === currentSmallVideo);
- Object.keys(remoteVideos).forEach(
- function (resourceJid) {
- var remoteVideo = remoteVideos[resourceJid];
- if (remoteVideo) {
- remoteVideo.enableDominantSpeaker(
- remoteVideo === currentSmallVideo);
- }
- }
- );
- };
-
- my.handleVideoThumbClicked = function(noPinnedEndpointChangedEvent,
- resourceJid) {
- if(focusedVideoResourceJid) {
- var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
- if (oldSmallVideo && !interfaceConfig.filmStripOnly)
- oldSmallVideo.focus(false);
- }
-
- var smallVideo = VideoLayout.getSmallVideo(resourceJid);
- // Unlock current focused.
- if (focusedVideoResourceJid === resourceJid)
- {
- focusedVideoResourceJid = null;
- // Enable the currently set dominant speaker.
- if (currentDominantSpeaker) {
- if(smallVideo && smallVideo.hasVideo()) {
- LargeVideo.updateLargeVideo(currentDominantSpeaker);
- }
- }
-
- if (!noPinnedEndpointChangedEvent) {
- eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
- }
- return;
- }
-
- // Lock new video
- focusedVideoResourceJid = resourceJid;
-
- // Update focused/pinned interface.
- if (resourceJid) {
- if (smallVideo && !interfaceConfig.filmStripOnly)
- smallVideo.focus(true);
-
- if (!noPinnedEndpointChangedEvent) {
- eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
- }
- }
-
- LargeVideo.setState("video");
-
- LargeVideo.updateLargeVideo(resourceJid);
-
- // Writing volume not allowed in IE
- if (!RTCBrowserType.isIExplorer()) {
- $('audio').each(function (idx, el) {
- el.volume = 0;
- el.volume = 1;
- });
- }
- };
-
-
- /**
- * Checks if container for participant identified by given peerJid exists
- * in the document and creates it eventually.
- *
- * @param peerJid peer Jid to check.
- *
- * @return Returns true if the peer container exists,
- * false - otherwise
- */
- my.ensurePeerContainerExists = function(peerJid) {
- ContactList.ensureAddContact(peerJid);
-
- var resourceJid = Strophe.getResourceFromJid(peerJid);
-
- if (!remoteVideos[resourceJid]) {
-
- var remoteVideo = new RemoteVideo(peerJid, VideoLayout);
- remoteVideos[resourceJid] = remoteVideo;
-
- var videoType = remoteVideoTypes[resourceJid];
- if (videoType) {
- remoteVideo.setVideoType(videoType);
- }
-
- // In case this is not currently in the last n we don't show it.
- if (localLastNCount &&
- localLastNCount > 0 &&
- $('#remoteVideos>span').length >= localLastNCount + 2) {
- remoteVideo.showPeerContainer('hide');
- }
- else
- VideoLayout.resizeThumbnails();
- }
- };
-
-
- my.inputDisplayNameHandler = function (name) {
- localVideoThumbnail.inputDisplayNameHandler(name);
- };
-
- my.videoactive = function (videoelem, resourceJid) {
-
- console.info(resourceJid + " video is now active");
-
- videoelem.show();
- VideoLayout.resizeThumbnails();
-
- // 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 ((!focusedVideoResourceJid &&
- !currentDominantSpeaker &&
- !require("../prezi/Prezi").isPresentationVisible()) ||
- focusedVideoResourceJid === resourceJid ||
- (resourceJid &&
- currentDominantSpeaker === resourceJid)) {
- LargeVideo.updateLargeVideo(resourceJid, true);
- }
- };
-
- /**
- * Shows the presence status message for the given video.
- */
- my.setPresenceStatus = function (resourceJid, statusMsg) {
- remoteVideos[resourceJid].setPresenceStatus(statusMsg);
- };
-
- /**
- * Shows a visual indicator for the moderator of the conference.
- */
- my.showModeratorIndicator = function () {
-
- var isModerator = APP.xmpp.isModerator();
- if (isModerator) {
- localVideoThumbnail.createModeratorIndicatorElement();
- }
-
- 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 member = members[jid];
-
- if (member.role === 'moderator') {
- remoteVideos[resourceJid].removeRemoteVideoMenu();
- remoteVideos[resourceJid].createModeratorIndicatorElement();
- } else if (isModerator) {
- // We are moderator, but user is not - add menu
- if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
- remoteVideos[resourceJid].addRemoteVideoMenu();
- }
- }
- });
- };
-
- /*
- * Shows or hides the audio muted indicator over the local thumbnail video.
- * @param {boolean} isMuted
- */
- my.showLocalAudioIndicator = function(isMuted) {
- localVideoThumbnail.showAudioIndicator(isMuted);
- };
-
- /**
- * Resizes the large video container.
- */
- my.resizeLargeVideoContainer = function () {
- if(LargeVideo.isEnabled()) {
- LargeVideo.resize();
- } else {
- VideoLayout.resizeVideoSpace();
- }
- VideoLayout.resizeThumbnails();
- LargeVideo.position();
- };
-
- /**
- * Resizes thumbnails.
- */
- my.resizeThumbnails = function(animate) {
- var videoSpaceWidth = $('#remoteVideos').width();
-
- var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
- var width = thumbnailSize[0];
- var height = thumbnailSize[1];
-
- $('.userAvatar').css('left', (width - height) / 2);
-
- if(animate) {
- $('#remoteVideos').animate({
- height: height + 2 // adds 2 px because of small video 1px border
- },
- {
- queue: false,
- duration: 500
- });
-
- $('#remoteVideos>span').animate({
- height: height,
- width: width
- },
- {
- queue: false,
- duration: 500,
- complete: function () {
- $(document).trigger(
- "remotevideo.resized",
- [width,
- height]);
- }
- });
-
- } else {
- // size videos so that while keeping AR and max height, we have a
- // nice fit
- $('#remoteVideos').height(height + 2);// adds 2 px because of small video 1px border
- $('#remoteVideos>span').width(width);
- $('#remoteVideos>span').height(height);
-
- $(document).trigger("remotevideo.resized", [width, height]);
- }
- };
-
- /**
- * 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, window.innerHeight - 18);
- if (availableHeight < availableWidth / aspectRatio) {
- availableWidth = Math.floor(availableHeight * aspectRatio);
- }
-
- return [availableWidth, availableHeight];
- };
-
- /**
- * 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);
- };
-
- my.getPeerVideoSel = function (peerResourceJid) {
- return $('#participant_' + peerResourceJid +
- '>' + APP.RTC.getVideoElementName());
- };
-
- /**
- * On contact list item clicked.
- */
- $(ContactList).bind('contactclicked', function(event, jid) {
- if (!jid) {
- return;
- }
-
- if (jid === APP.xmpp.myJid()) {
- $("#localVideoContainer").click();
- return;
- }
-
- var resource = Strophe.getResourceFromJid(jid);
- var videoSel = VideoLayout.getPeerVideoSel(resource);
- if (videoSel.length > 0) {
- var videoThumb = videoSel[0];
- // It is not always the case that a videoThumb exists (if there is
- // no actual video).
- if (RTC.getVideoSrc(videoThumb)) {
-
- // We have a video src, great! Let's update the large video
- // now.
- VideoLayout.handleVideoThumbClicked(
- 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));
- }
- }
- });
-
- /**
- * On audio muted event.
- */
- my.onAudioMute = function (jid, isMuted) {
- var resourceJid = Strophe.getResourceFromJid(jid);
- if (resourceJid === APP.xmpp.myResource()) {
- localVideoThumbnail.showAudioIndicator(isMuted);
- } else {
- VideoLayout.ensurePeerContainerExists(jid);
- remoteVideos[resourceJid].showAudioIndicator(isMuted);
- if (APP.xmpp.isModerator()) {
- remoteVideos[resourceJid].updateRemoteVideoMenu(isMuted);
- }
- }
- };
-
- /**
- * On video muted event.
- */
- my.onVideoMute = function (jid, value) {
- if (jid !== APP.xmpp.myJid() &&
- !APP.RTC.muteRemoteVideoStream(jid, value))
- return;
-
- if (jid === APP.xmpp.myJid()) {
- localVideoThumbnail.showVideoIndicator(value);
- } else {
- var resource = Strophe.getResourceFromJid(jid);
-
- VideoLayout.ensurePeerContainerExists(jid);
- remoteVideos[resource].showVideoIndicator(value);
-
- var el = VideoLayout.getPeerVideoSel(resource);
- if (!value)
- el.show();
- else
- el.hide();
- }
- };
-
- /**
- * Display name changed.
- */
- my.onDisplayNameChanged =
- function (jid, displayName, status) {
- if (jid === 'localVideoContainer' ||
- jid === APP.xmpp.myJid()) {
- localVideoThumbnail.setDisplayName(displayName);
- } else {
- VideoLayout.ensurePeerContainerExists(jid);
- remoteVideos[Strophe.getResourceFromJid(jid)].setDisplayName(
- 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 currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker);
- var newJID = APP.xmpp.findJidFromResource(resourceJid);
- if(currentDominantSpeaker && (!members || !members[currentJID] ||
- !members[currentJID].displayName) && remoteVideos[resourceJid]) {
- remoteVideos[resourceJid].setDisplayName(null);
- }
- if(resourceJid && (!members || !members[newJID] ||
- !members[newJID].displayName) && remoteVideos[resourceJid]) {
- remoteVideos[resourceJid].setDisplayName(null,
- interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME);
- }
- currentDominantSpeaker = resourceJid;
- } else {
- return;
- }
-
- // Obtain container for new dominant speaker.
- var videoSel = VideoLayout.getPeerVideoSel(resourceJid);
-
- // Local video will not have container found, but that's ok
- // since we don't want to switch to local video.
- if (!focusedVideoResourceJid && videoSel.length) {
- // Update the large video if the video source is already available,
- // otherwise wait for the "videoactive.jingle" event.
- if (videoSel[0].currentTime > 0) {
- LargeVideo.updateLargeVideo(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);
- remoteVideos[resourceJid].showPeerContainer('hide');
- isReceived = false;
- } else if (resourceJid &&
- $('#participant_' + resourceJid).is(':visible') &&
- lastNEndpoints.indexOf(resourceJid) < 0 &&
- localLastNSet.indexOf(resourceJid) >= 0) {
- remoteVideos[resourceJid].showPeerContainer('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.
- if (!updateLargeVideo &&
- resourceJid === LargeVideo.getResourceJid()) {
- updateLargeVideo = true;
- }
- }
- });
-
- if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
- endpointsEnteringLastN = lastNEndpoints;
-
- if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
- endpointsEnteringLastN.forEach(function (resourceJid) {
-
- var isVisible = $('#participant_' + resourceJid).is(':visible');
- remoteVideos[resourceJid].showPeerContainer('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 = VideoLayout.getPeerVideoSel(resourceJid);
-
- APP.RTC.attachMediaStream(sel, mediaStream.stream);
- 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(
- false,
- Strophe.getResourceFromJid(mediaStream.peerjid));
-
- updateLargeVideo = false;
- }
- remoteVideos[resourceJid].
- waitForPlayback(sel, mediaStream.stream);
- }
- });
- }
-
- // 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;
- var myResource
- = APP.xmpp.myResource();
-
- // Find out which endpoint to show in the large video.
- for (i = 0; i < lastNEndpoints.length; i++) {
- resource = lastNEndpoints[i];
- if (!resource || resource === myResource)
- continue;
-
- // videoSrcToSsrc needs to be update for this call to succeed.
- LargeVideo.updateLargeVideo(resource);
- break;
- }
- }
- };
-
- /**
- * 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()];
- }
- localVideoThumbnail.updateStatsIndicator(percent, object);
- for (var jid in resolution) {
- if (resolution[jid] === null)
- continue;
- var resourceJid = Strophe.getResourceFromJid(jid);
- if (remoteVideos[resourceJid] &&
- remoteVideos[resourceJid].connectionIndicator) {
- remoteVideos[resourceJid].connectionIndicator.
- 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);
-
- if (remoteVideos[resourceJid])
- remoteVideos[resourceJid].updateStatsIndicator(percent, object);
- };
-
- /**
- * Hides the connection indicator
- * @param jid
- */
- my.hideConnectionIndicator = function (jid) {
- remoteVideos[Strophe.getResourceFromJid(jid)].hideConnectionIndicator();
- };
-
- /**
- * Hides all the indicators
- */
- my.onStatsStop = function () {
- for(var video in remoteVideos) {
- remoteVideos[video].hideIndicator();
- }
- localVideoThumbnail.hideIndicator();
- };
-
- my.participantLeft = function (jid) {
- // Unlock large video
- var resourceJid = Strophe.getResourceFromJid(jid);
- if (focusedVideoResourceJid === resourceJid) {
- console.info("Focused video owner has left the conference");
- focusedVideoResourceJid = null;
- }
-
- if (currentDominantSpeaker === resourceJid)
- {
- console.info("Dominant speaker has left the conference");
- currentDominantSpeaker = null;
- }
-
- var remoteVideo = remoteVideos[resourceJid];
- if (remoteVideo) {
- // Remove remote video
- console.info("Removing remote video: " + resourceJid);
- remoteVideo.remove();
- delete remoteVideos[resourceJid];
- } else {
- console.warn("No remote video for " + resourceJid);
- }
-
- VideoLayout.resizeThumbnails();
- };
-
- my.onVideoTypeChanged = function (resourceJid, newVideoType) {
- if (remoteVideoTypes[resourceJid] === newVideoType) {
- return;
- }
-
- console.info("Peer video type changed: ", resourceJid, newVideoType);
- remoteVideoTypes[resourceJid] = newVideoType;
-
- var smallVideo;
- if (resourceJid === APP.xmpp.myResource()) {
- if (!localVideoThumbnail) {
- console.warn("Local video not ready yet");
- return;
- }
- smallVideo = localVideoThumbnail;
- } else if (remoteVideos[resourceJid]) {
- smallVideo = remoteVideos[resourceJid];
- } else {
- return;
- }
-
- smallVideo.setVideoType(newVideoType);
- LargeVideo.onVideoTypeChanged(resourceJid, newVideoType);
-
- };
-
- my.showMore = function (jid) {
- if (jid === 'local') {
- localVideoThumbnail.connectionIndicator.showMore();
- } else {
- var remoteVideo = remoteVideos[Strophe.getResourceFromJid(jid)];
- if (remoteVideo) {
- remoteVideo.connectionIndicator.showMore();
- } else {
- console.info("Error - no remote video for jid: " + jid);
- }
- }
- };
-
- my.addPreziContainer = function (id) {
- var container = RemoteVideo.createContainer(id);
- VideoLayout.resizeThumbnails();
- return container;
- };
-
- my.setLargeVideoVisible = function (isVisible) {
- LargeVideo.setLargeVideoVisible(isVisible);
- if(!isVisible && focusedVideoResourceJid) {
- var smallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
- if(smallVideo) {
- smallVideo.focus(false);
- smallVideo.showAvatar();
- }
- focusedVideoResourceJid = null;
- }
- };
-
- /**
- * Resizes the video area
- * @param callback a function to be called when the video space is
- * resized.
- */
- my.resizeVideoArea = function(isVisible, callback) {
- LargeVideo.resizeVideoAreaAnimated(isVisible, callback);
- VideoLayout.resizeThumbnails(true);
- };
-
- /**
- * Resizes the #videospace html element
- * @param animate boolean property that indicates whether the resize should be animated or not.
- * @param isChatVisible boolean property that indicates whether the chat area is displayed or not.
- * If that parameter is null the method will check the chat pannel visibility.
- * @param completeFunction a function to be called when the video space is resized
- */
- my.resizeVideoSpace = function (animate, isChatVisible, completeFunction) {
- var availableHeight = window.innerHeight;
- var availableWidth = UIUtil.getAvailableVideoWidth(isChatVisible);
-
- if (availableWidth < 0 || availableHeight < 0) return;
-
- if(animate) {
- $('#videospace').animate({
- right: window.innerWidth - availableWidth,
- width: availableWidth,
- height: availableHeight
- },
- {
- queue: false,
- duration: 500,
- complete: completeFunction
- });
- } else {
- $('#videospace').width(availableWidth);
- $('#videospace').height(availableHeight);
- }
-
- };
-
- my.getSmallVideo = function (resourceJid) {
- if(resourceJid == APP.xmpp.myResource()) {
- return localVideoThumbnail;
- } else {
- if(!remoteVideos[resourceJid])
- return null;
- return remoteVideos[resourceJid];
- }
- };
-
- my.userAvatarChanged = function(resourceJid, thumbUrl) {
- var smallVideo = VideoLayout.getSmallVideo(resourceJid);
- if(smallVideo)
- smallVideo.avatarChanged(thumbUrl);
- else
- console.warn(
- "Missed avatar update - no small video yet for " + resourceJid);
- LargeVideo.updateAvatar(resourceJid, thumbUrl);
- };
-
- my.createEtherpadIframe = function(src, onloadHandler)
- {
- return LargeVideo.createEtherpadIframe(src, onloadHandler);
- };
-
- my.setLargeVideoState = function (state) {
- LargeVideo.setState(state);
- };
-
- my.getLargeVideoState = function () {
- return LargeVideo.getState();
- };
-
- my.setLargeVideoHover = function (inHandler, outHandler) {
- LargeVideo.setHover(inHandler, outHandler);
- };
-
- /**
- * Indicates that the video has been interrupted.
- */
- my.onVideoInterrupted = function () {
- LargeVideo.enableVideoProblemFilter(true);
- var reconnectingKey = "connection.RECONNECTING";
- $('#videoConnectionMessage').attr("data-i18n", reconnectingKey);
- $('#videoConnectionMessage').text(APP.translation.translateString(reconnectingKey));
- $('#videoConnectionMessage').css({display: "block"});
- };
-
- /**
- * Indicates that the video has been restored.
- */
- my.onVideoRestored = function () {
- LargeVideo.enableVideoProblemFilter(false);
- $('#videoConnectionMessage').css({display: "none"});
- };
-
- return my;
-}(VideoLayout || {}));
-
+},{"../../RTC/RTCBrowserType":8,"../avatar/Avatar":16,"../util/UIUtil":33,"./LargeVideo":35}],39:[function(require,module,exports){
+/* global config, APP, $, Strophe, require, interfaceConfig */
+var AudioLevels = require("../audio_levels/AudioLevels");
+var ContactList = require("../side_pannels/contactlist/ContactList");
+var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
+var UIEvents = require("../../../service/UI/UIEvents");
+var UIUtil = require("../util/UIUtil");
+
+var RTC = require("../../RTC/RTC");
+var RTCBrowserType = require('../../RTC/RTCBrowserType');
+
+var RemoteVideo = require("./RemoteVideo");
+var LargeVideo = require("./LargeVideo");
+var LocalVideo = require("./LocalVideo");
+
+
+var remoteVideos = {};
+var remoteVideoTypes = {};
+var localVideoThumbnail = null;
+
+var currentDominantSpeaker = null;
+var lastNCount = config.channelLastN;
+var localLastNCount = config.channelLastN;
+var localLastNSet = [];
+var lastNEndpointsCache = [];
+var lastNPickupJid = null;
+
+var eventEmitter = null;
+
+/**
+ * Currently focused video jid
+ * @type {String}
+ */
+var focusedVideoResourceJid = null;
+
+var VideoLayout = (function (my) {
+ my.init = function (emitter) {
+ eventEmitter = emitter;
+ localVideoThumbnail = new LocalVideo(VideoLayout);
+ if (interfaceConfig.filmStripOnly) {
+ LargeVideo.disable();
+ } else {
+ LargeVideo.init(VideoLayout, emitter);
+ }
+
+ VideoLayout.resizeLargeVideoContainer();
+
+ };
+
+ my.isInLastN = function(resource) {
+ return lastNCount < 0 || // lastN is disabled
+ // lastNEndpoints cache not built yet
+ (lastNCount > 0 && lastNEndpointsCache.length == 0) ||
+ (lastNEndpointsCache &&
+ lastNEndpointsCache.indexOf(resource) !== -1);
+ };
+
+ my.changeLocalStream = function (stream, isMuted) {
+ VideoLayout.changeLocalVideo(stream, isMuted);
+ };
+
+ my.changeLocalAudio = function(stream, isMuted) {
+ if (isMuted)
+ APP.UI.setAudioMuted(true, true);
+ APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
+ var localAudio = document.getElementById('localAudio');
+ // Writing volume not allowed in IE
+ if (!RTCBrowserType.isIExplorer()) {
+ localAudio.autoplay = true;
+ localAudio.volume = 0;
+ }
+ };
+
+ my.changeLocalVideo = function(stream, isMuted) {
+ // Set default display name.
+ localVideoThumbnail.setDisplayName();
+ localVideoThumbnail.createConnectionIndicator();
+
+ this.onVideoTypeChanged(APP.xmpp.myResource(), stream.videoType);
+
+ AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
+
+ localVideoThumbnail.changeVideo(stream, isMuted);
+
+ /* force update if we're currently being displayed */
+ if (LargeVideo.isCurrentlyOnLarge(APP.xmpp.myResource())) {
+ LargeVideo.updateLargeVideo(APP.xmpp.myResource(), true);
+ }
+ };
+
+ my.mucJoined = function () {
+ var myResourceJid = APP.xmpp.myResource();
+ localVideoThumbnail.joined(APP.xmpp.myJid());
+
+ if (!LargeVideo.getResourceJid())
+ LargeVideo.updateLargeVideo(myResourceJid, true);
+ };
+
+ /**
+ * Adds or removes icons for not available camera and microphone.
+ * @param resourceJid the jid of user
+ * @param devices available devices
+ */
+ my.setDeviceAvailabilityIcons = function (resourceJid, devices) {
+ if(!devices)
+ return;
+
+ if(!resourceJid) {
+ localVideoThumbnail.setDeviceAvailabilityIcons(devices);
+ } else {
+ if(remoteVideos[resourceJid])
+ remoteVideos[resourceJid].setDeviceAvailabilityIcons(devices);
+ }
+ };
+
+ /**
+ * 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(resourceJid) {
+
+ if (resourceJid === LargeVideo.getResourceJid()) {
+ var newResourceJid;
+ // We'll show user's avatar if he is the dominant speaker or if
+ // his video thumbnail is pinned
+ if (remoteVideos[resourceJid] &&
+ resourceJid === focusedVideoResourceJid ||
+ resourceJid === currentDominantSpeaker) {
+ newResourceJid = resourceJid;
+ } else {
+ // Otherwise select last visible video
+ newResourceJid = this.electLastVisibleVideo();
+ }
+ LargeVideo.updateLargeVideo(newResourceJid);
+ }
+ };
+
+ my.electLastVisibleVideo = function() {
+ // pick the last visible video in the row
+ // if nobody else is left, this picks the local video
+ var jid;
+ var videoElem = RTC.getVideoElementName();
+ var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>' + videoElem);
+ if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
+ jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
+ } else {
+ console.info("Last visible video no longer exists");
+ pick = $('#remoteVideos>span[id!="mixedstream"]>' + videoElem);
+ if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
+ jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
+ } else {
+ // Try local video
+ console.info("Fallback to local video...");
+ jid = APP.xmpp.myResource();
+ }
+ }
+ console.info("electLastVisibleVideo: " + jid);
+ return jid;
+ };
+
+ my.onRemoteStreamAdded = function (stream) {
+ if (stream.peerjid) {
+ VideoLayout.ensurePeerContainerExists(stream.peerjid);
+
+ var resourceJid = Strophe.getResourceFromJid(stream.peerjid);
+ remoteVideos[resourceJid].addRemoteStreamElement(
+ stream.sid,
+ stream.getOriginalStream(),
+ stream.ssrc);
+ }
+ };
+
+ my.getLargeVideoResource = function () {
+ return LargeVideo.getResourceJid();
+ };
+
+ /**
+ * Called when large video update is finished
+ * @param currentSmallVideo small video currently displayed on large video
+ */
+ my.largeVideoUpdated = function (currentSmallVideo) {
+ // Makes sure that dominant speaker UI
+ // is enabled only on current small video
+ localVideoThumbnail.enableDominantSpeaker(
+ localVideoThumbnail === currentSmallVideo);
+ Object.keys(remoteVideos).forEach(
+ function (resourceJid) {
+ var remoteVideo = remoteVideos[resourceJid];
+ if (remoteVideo) {
+ remoteVideo.enableDominantSpeaker(
+ remoteVideo === currentSmallVideo);
+ }
+ }
+ );
+ };
+
+ my.handleVideoThumbClicked = function(noPinnedEndpointChangedEvent,
+ resourceJid) {
+ if(focusedVideoResourceJid) {
+ var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
+ if (oldSmallVideo && !interfaceConfig.filmStripOnly)
+ oldSmallVideo.focus(false);
+ }
+
+ var smallVideo = VideoLayout.getSmallVideo(resourceJid);
+ // Unlock current focused.
+ if (focusedVideoResourceJid === resourceJid)
+ {
+ focusedVideoResourceJid = null;
+ // Enable the currently set dominant speaker.
+ if (currentDominantSpeaker) {
+ if(smallVideo && smallVideo.hasVideo()) {
+ LargeVideo.updateLargeVideo(currentDominantSpeaker);
+ }
+ }
+
+ if (!noPinnedEndpointChangedEvent) {
+ eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
+ }
+ return;
+ }
+
+ // Lock new video
+ focusedVideoResourceJid = resourceJid;
+
+ // Update focused/pinned interface.
+ if (resourceJid) {
+ if (smallVideo && !interfaceConfig.filmStripOnly)
+ smallVideo.focus(true);
+
+ if (!noPinnedEndpointChangedEvent) {
+ eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
+ }
+ }
+
+ LargeVideo.setState("video");
+
+ LargeVideo.updateLargeVideo(resourceJid);
+
+ // Writing volume not allowed in IE
+ if (!RTCBrowserType.isIExplorer()) {
+ $('audio').each(function (idx, el) {
+ el.volume = 0;
+ el.volume = 1;
+ });
+ }
+ };
+
+
+ /**
+ * Checks if container for participant identified by given peerJid exists
+ * in the document and creates it eventually.
+ *
+ * @param peerJid peer Jid to check.
+ *
+ * @return Returns true if the peer container exists,
+ * false - otherwise
+ */
+ my.ensurePeerContainerExists = function(peerJid) {
+ ContactList.ensureAddContact(peerJid);
+
+ var resourceJid = Strophe.getResourceFromJid(peerJid);
+
+ if (!remoteVideos[resourceJid]) {
+
+ var remoteVideo = new RemoteVideo(peerJid, VideoLayout);
+ remoteVideos[resourceJid] = remoteVideo;
+
+ var videoType = remoteVideoTypes[resourceJid];
+ if (videoType) {
+ remoteVideo.setVideoType(videoType);
+ }
+
+ // In case this is not currently in the last n we don't show it.
+ if (localLastNCount &&
+ localLastNCount > 0 &&
+ $('#remoteVideos>span').length >= localLastNCount + 2) {
+ remoteVideo.showPeerContainer('hide');
+ }
+ else
+ VideoLayout.resizeThumbnails();
+ }
+ };
+
+
+ my.inputDisplayNameHandler = function (name) {
+ localVideoThumbnail.inputDisplayNameHandler(name);
+ };
+
+ my.videoactive = function (videoelem, resourceJid) {
+
+ console.info(resourceJid + " video is now active");
+
+ videoelem.show();
+ VideoLayout.resizeThumbnails();
+
+ // 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 ((!focusedVideoResourceJid &&
+ !currentDominantSpeaker &&
+ !require("../prezi/Prezi").isPresentationVisible()) ||
+ focusedVideoResourceJid === resourceJid ||
+ (resourceJid &&
+ currentDominantSpeaker === resourceJid)) {
+ LargeVideo.updateLargeVideo(resourceJid, true);
+ }
+ };
+
+ /**
+ * Shows the presence status message for the given video.
+ */
+ my.setPresenceStatus = function (resourceJid, statusMsg) {
+ remoteVideos[resourceJid].setPresenceStatus(statusMsg);
+ };
+
+ /**
+ * Shows a visual indicator for the moderator of the conference.
+ */
+ my.showModeratorIndicator = function () {
+
+ var isModerator = APP.xmpp.isModerator();
+ if (isModerator) {
+ localVideoThumbnail.createModeratorIndicatorElement();
+ }
+
+ 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 member = members[jid];
+
+ if (member.role === 'moderator') {
+ remoteVideos[resourceJid].removeRemoteVideoMenu();
+ remoteVideos[resourceJid].createModeratorIndicatorElement();
+ } else if (isModerator) {
+ // We are moderator, but user is not - add menu
+ if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
+ remoteVideos[resourceJid].addRemoteVideoMenu();
+ }
+ }
+ });
+ };
+
+ /*
+ * Shows or hides the audio muted indicator over the local thumbnail video.
+ * @param {boolean} isMuted
+ */
+ my.showLocalAudioIndicator = function(isMuted) {
+ localVideoThumbnail.showAudioIndicator(isMuted);
+ };
+
+ /**
+ * Resizes the large video container.
+ */
+ my.resizeLargeVideoContainer = function () {
+ if(LargeVideo.isEnabled()) {
+ LargeVideo.resize();
+ } else {
+ VideoLayout.resizeVideoSpace();
+ }
+ VideoLayout.resizeThumbnails();
+ LargeVideo.position();
+ };
+
+ /**
+ * Resizes thumbnails.
+ */
+ my.resizeThumbnails = function(animate) {
+ var videoSpaceWidth = $('#remoteVideos').width();
+
+ var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
+ var width = thumbnailSize[0];
+ var height = thumbnailSize[1];
+
+ $('.userAvatar').css('left', (width - height) / 2);
+
+ if(animate) {
+ $('#remoteVideos').animate({
+ height: height + 2 // adds 2 px because of small video 1px border
+ },
+ {
+ queue: false,
+ duration: 500
+ });
+
+ $('#remoteVideos>span').animate({
+ height: height,
+ width: width
+ },
+ {
+ queue: false,
+ duration: 500,
+ complete: function () {
+ $(document).trigger(
+ "remotevideo.resized",
+ [width,
+ height]);
+ }
+ });
+
+ } else {
+ // size videos so that while keeping AR and max height, we have a
+ // nice fit
+ $('#remoteVideos').height(height + 2);// adds 2 px because of small video 1px border
+ $('#remoteVideos>span').width(width);
+ $('#remoteVideos>span').height(height);
+
+ $(document).trigger("remotevideo.resized", [width, height]);
+ }
+ };
+
+ /**
+ * 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, window.innerHeight - 18);
+ if (availableHeight < availableWidth / aspectRatio) {
+ availableWidth = Math.floor(availableHeight * aspectRatio);
+ }
+
+ return [availableWidth, availableHeight];
+ };
+
+ /**
+ * 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);
+ };
+
+ my.getPeerVideoSel = function (peerResourceJid) {
+ return $('#participant_' + peerResourceJid +
+ '>' + APP.RTC.getVideoElementName());
+ };
+
+ /**
+ * On contact list item clicked.
+ */
+ $(ContactList).bind('contactclicked', function(event, jid) {
+ if (!jid) {
+ return;
+ }
+
+ if (jid === APP.xmpp.myJid()) {
+ $("#localVideoContainer").click();
+ return;
+ }
+
+ var resource = Strophe.getResourceFromJid(jid);
+ var videoSel = VideoLayout.getPeerVideoSel(resource);
+ if (videoSel.length > 0) {
+ var videoThumb = videoSel[0];
+ // It is not always the case that a videoThumb exists (if there is
+ // no actual video).
+ if (RTC.getVideoSrc(videoThumb)) {
+
+ // We have a video src, great! Let's update the large video
+ // now.
+ VideoLayout.handleVideoThumbClicked(
+ 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));
+ }
+ }
+ });
+
+ /**
+ * On audio muted event.
+ */
+ my.onAudioMute = function (jid, isMuted) {
+ var resourceJid = Strophe.getResourceFromJid(jid);
+ if (resourceJid === APP.xmpp.myResource()) {
+ localVideoThumbnail.showAudioIndicator(isMuted);
+ } else {
+ VideoLayout.ensurePeerContainerExists(jid);
+ remoteVideos[resourceJid].showAudioIndicator(isMuted);
+ if (APP.xmpp.isModerator()) {
+ remoteVideos[resourceJid].updateRemoteVideoMenu(isMuted);
+ }
+ }
+ };
+
+ /**
+ * On video muted event.
+ */
+ my.onVideoMute = function (jid, value) {
+ if (jid !== APP.xmpp.myJid() &&
+ !APP.RTC.muteRemoteVideoStream(jid, value))
+ return;
+
+ if (jid === APP.xmpp.myJid()) {
+ localVideoThumbnail.showVideoIndicator(value);
+ } else {
+ var resource = Strophe.getResourceFromJid(jid);
+
+ VideoLayout.ensurePeerContainerExists(jid);
+ remoteVideos[resource].showVideoIndicator(value);
+
+ var el = VideoLayout.getPeerVideoSel(resource);
+ if (!value)
+ el.show();
+ else
+ el.hide();
+ }
+ };
+
+ /**
+ * Display name changed.
+ */
+ my.onDisplayNameChanged =
+ function (jid, displayName, status) {
+ if (jid === 'localVideoContainer' ||
+ jid === APP.xmpp.myJid()) {
+ localVideoThumbnail.setDisplayName(displayName);
+ } else {
+ VideoLayout.ensurePeerContainerExists(jid);
+ remoteVideos[Strophe.getResourceFromJid(jid)].setDisplayName(
+ 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 currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker);
+ var newJID = APP.xmpp.findJidFromResource(resourceJid);
+ if(currentDominantSpeaker && (!members || !members[currentJID] ||
+ !members[currentJID].displayName) && remoteVideos[resourceJid]) {
+ remoteVideos[resourceJid].setDisplayName(null);
+ }
+ if(resourceJid && (!members || !members[newJID] ||
+ !members[newJID].displayName) && remoteVideos[resourceJid]) {
+ remoteVideos[resourceJid].setDisplayName(null,
+ interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME);
+ }
+ currentDominantSpeaker = resourceJid;
+ } else {
+ return;
+ }
+
+ // Obtain container for new dominant speaker.
+ var videoSel = VideoLayout.getPeerVideoSel(resourceJid);
+
+ // Local video will not have container found, but that's ok
+ // since we don't want to switch to local video.
+ if (!focusedVideoResourceJid && videoSel.length) {
+ // Update the large video if the video source is already available,
+ // otherwise wait for the "videoactive.jingle" event.
+ if (videoSel[0].currentTime > 0) {
+ LargeVideo.updateLargeVideo(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);
+ remoteVideos[resourceJid].showPeerContainer('hide');
+ isReceived = false;
+ } else if (resourceJid &&
+ $('#participant_' + resourceJid).is(':visible') &&
+ lastNEndpoints.indexOf(resourceJid) < 0 &&
+ localLastNSet.indexOf(resourceJid) >= 0) {
+ remoteVideos[resourceJid].showPeerContainer('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.
+ if (!updateLargeVideo &&
+ resourceJid === LargeVideo.getResourceJid()) {
+ updateLargeVideo = true;
+ }
+ }
+ });
+
+ if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
+ endpointsEnteringLastN = lastNEndpoints;
+
+ if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
+ endpointsEnteringLastN.forEach(function (resourceJid) {
+
+ var isVisible = $('#participant_' + resourceJid).is(':visible');
+ remoteVideos[resourceJid].showPeerContainer('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 = VideoLayout.getPeerVideoSel(resourceJid);
+
+ APP.RTC.attachMediaStream(sel, mediaStream.stream);
+ 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(
+ false,
+ Strophe.getResourceFromJid(mediaStream.peerjid));
+
+ updateLargeVideo = false;
+ }
+ remoteVideos[resourceJid].
+ waitForPlayback(sel, mediaStream.stream);
+ }
+ });
+ }
+
+ // 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;
+ var myResource
+ = APP.xmpp.myResource();
+
+ // Find out which endpoint to show in the large video.
+ for (i = 0; i < lastNEndpoints.length; i++) {
+ resource = lastNEndpoints[i];
+ if (!resource || resource === myResource)
+ continue;
+
+ // videoSrcToSsrc needs to be update for this call to succeed.
+ LargeVideo.updateLargeVideo(resource);
+ break;
+ }
+ }
+ };
+
+ /**
+ * 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()];
+ }
+ localVideoThumbnail.updateStatsIndicator(percent, object);
+ for (var jid in resolution) {
+ if (resolution[jid] === null)
+ continue;
+ var resourceJid = Strophe.getResourceFromJid(jid);
+ if (remoteVideos[resourceJid] &&
+ remoteVideos[resourceJid].connectionIndicator) {
+ remoteVideos[resourceJid].connectionIndicator.
+ 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);
+
+ if (remoteVideos[resourceJid])
+ remoteVideos[resourceJid].updateStatsIndicator(percent, object);
+ };
+
+ /**
+ * Hides the connection indicator
+ * @param jid
+ */
+ my.hideConnectionIndicator = function (jid) {
+ remoteVideos[Strophe.getResourceFromJid(jid)].hideConnectionIndicator();
+ };
+
+ /**
+ * Hides all the indicators
+ */
+ my.onStatsStop = function () {
+ for(var video in remoteVideos) {
+ remoteVideos[video].hideIndicator();
+ }
+ localVideoThumbnail.hideIndicator();
+ };
+
+ my.participantLeft = function (jid) {
+ // Unlock large video
+ var resourceJid = Strophe.getResourceFromJid(jid);
+ if (focusedVideoResourceJid === resourceJid) {
+ console.info("Focused video owner has left the conference");
+ focusedVideoResourceJid = null;
+ }
+
+ if (currentDominantSpeaker === resourceJid)
+ {
+ console.info("Dominant speaker has left the conference");
+ currentDominantSpeaker = null;
+ }
+
+ var remoteVideo = remoteVideos[resourceJid];
+ if (remoteVideo) {
+ // Remove remote video
+ console.info("Removing remote video: " + resourceJid);
+ remoteVideo.remove();
+ delete remoteVideos[resourceJid];
+ } else {
+ console.warn("No remote video for " + resourceJid);
+ }
+
+ VideoLayout.resizeThumbnails();
+ };
+
+ my.onVideoTypeChanged = function (resourceJid, newVideoType) {
+ if (remoteVideoTypes[resourceJid] === newVideoType) {
+ return;
+ }
+
+ console.info("Peer video type changed: ", resourceJid, newVideoType);
+ remoteVideoTypes[resourceJid] = newVideoType;
+
+ var smallVideo;
+ if (resourceJid === APP.xmpp.myResource()) {
+ if (!localVideoThumbnail) {
+ console.warn("Local video not ready yet");
+ return;
+ }
+ smallVideo = localVideoThumbnail;
+ } else if (remoteVideos[resourceJid]) {
+ smallVideo = remoteVideos[resourceJid];
+ } else {
+ return;
+ }
+
+ smallVideo.setVideoType(newVideoType);
+ LargeVideo.onVideoTypeChanged(resourceJid, newVideoType);
+
+ };
+
+ my.showMore = function (jid) {
+ if (jid === 'local') {
+ localVideoThumbnail.connectionIndicator.showMore();
+ } else {
+ var remoteVideo = remoteVideos[Strophe.getResourceFromJid(jid)];
+ if (remoteVideo) {
+ remoteVideo.connectionIndicator.showMore();
+ } else {
+ console.info("Error - no remote video for jid: " + jid);
+ }
+ }
+ };
+
+ my.addPreziContainer = function (id) {
+ var container = RemoteVideo.createContainer(id);
+ VideoLayout.resizeThumbnails();
+ return container;
+ };
+
+ my.setLargeVideoVisible = function (isVisible) {
+ LargeVideo.setLargeVideoVisible(isVisible);
+ if(!isVisible && focusedVideoResourceJid) {
+ var smallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
+ if(smallVideo) {
+ smallVideo.focus(false);
+ smallVideo.showAvatar();
+ }
+ focusedVideoResourceJid = null;
+ }
+ };
+
+ /**
+ * Resizes the video area
+ * @param callback a function to be called when the video space is
+ * resized.
+ */
+ my.resizeVideoArea = function(isVisible, callback) {
+ LargeVideo.resizeVideoAreaAnimated(isVisible, callback);
+ VideoLayout.resizeThumbnails(true);
+ };
+
+ /**
+ * Resizes the #videospace html element
+ * @param animate boolean property that indicates whether the resize should be animated or not.
+ * @param isChatVisible boolean property that indicates whether the chat area is displayed or not.
+ * If that parameter is null the method will check the chat pannel visibility.
+ * @param completeFunction a function to be called when the video space is resized
+ */
+ my.resizeVideoSpace = function (animate, isChatVisible, completeFunction) {
+ var availableHeight = window.innerHeight;
+ var availableWidth = UIUtil.getAvailableVideoWidth(isChatVisible);
+
+ if (availableWidth < 0 || availableHeight < 0) return;
+
+ if(animate) {
+ $('#videospace').animate({
+ right: window.innerWidth - availableWidth,
+ width: availableWidth,
+ height: availableHeight
+ },
+ {
+ queue: false,
+ duration: 500,
+ complete: completeFunction
+ });
+ } else {
+ $('#videospace').width(availableWidth);
+ $('#videospace').height(availableHeight);
+ }
+
+ };
+
+ my.getSmallVideo = function (resourceJid) {
+ if(resourceJid == APP.xmpp.myResource()) {
+ return localVideoThumbnail;
+ } else {
+ if(!remoteVideos[resourceJid])
+ return null;
+ return remoteVideos[resourceJid];
+ }
+ };
+
+ my.userAvatarChanged = function(resourceJid, thumbUrl) {
+ var smallVideo = VideoLayout.getSmallVideo(resourceJid);
+ if(smallVideo)
+ smallVideo.avatarChanged(thumbUrl);
+ else
+ console.warn(
+ "Missed avatar update - no small video yet for " + resourceJid);
+ LargeVideo.updateAvatar(resourceJid, thumbUrl);
+ };
+
+ my.createEtherpadIframe = function(src, onloadHandler)
+ {
+ return LargeVideo.createEtherpadIframe(src, onloadHandler);
+ };
+
+ my.setLargeVideoState = function (state) {
+ LargeVideo.setState(state);
+ };
+
+ my.getLargeVideoState = function () {
+ return LargeVideo.getState();
+ };
+
+ my.setLargeVideoHover = function (inHandler, outHandler) {
+ LargeVideo.setHover(inHandler, outHandler);
+ };
+
+ /**
+ * Indicates that the video has been interrupted.
+ */
+ my.onVideoInterrupted = function () {
+ LargeVideo.enableVideoProblemFilter(true);
+ var reconnectingKey = "connection.RECONNECTING";
+ $('#videoConnectionMessage').attr("data-i18n", reconnectingKey);
+ $('#videoConnectionMessage').text(APP.translation.translateString(reconnectingKey));
+ $('#videoConnectionMessage').css({display: "block"});
+ };
+
+ /**
+ * Indicates that the video has been restored.
+ */
+ my.onVideoRestored = function () {
+ LargeVideo.enableVideoProblemFilter(false);
+ $('#videoConnectionMessage').css({display: "none"});
+ };
+
+ return my;
+}(VideoLayout || {}));
+
module.exports = VideoLayout;
-},{"../../../service/RTC/MediaStreamTypes":156,"../../../service/UI/UIEvents":160,"../../RTC/RTC":9,"../../RTC/RTCBrowserType":10,"../audio_levels/AudioLevels":14,"../prezi/Prezi":20,"../side_pannels/contactlist/ContactList":27,"../util/UIUtil":35,"./LargeVideo":37,"./LocalVideo":38,"./RemoteVideo":39}],42:[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 have a uniform distribution amongst all patterns (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;
+},{"../../../service/RTC/MediaStreamTypes":159,"../../../service/UI/UIEvents":163,"../../RTC/RTC":7,"../../RTC/RTCBrowserType":8,"../audio_levels/AudioLevels":12,"../prezi/Prezi":18,"../side_pannels/contactlist/ContactList":25,"../util/UIUtil":33,"./LargeVideo":35,"./LocalVideo":36,"./RemoteVideo":37}],40:[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 have a uniform distribution amongst all patterns (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;
+
+},{}],41:[function(require,module,exports){
+/* global $, interfaceConfig */
+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;
+ var selector = $("#reload_roomname");
+ selector.click(function () {
+ clearTimeout(updateTimeout);
+ clearTimeout(animateTimeout);
+ update_roomname();
+ });
+ selector.show();
+
+ update_roomname();
+ }
+
+ $("#disable_welcome").click(function () {
+ window.localStorage.welcomePageDisabled =
+ $("#disable_welcome").is(":checked");
+ });
+
+}
-},{}],43:[function(require,module,exports){
-/* global $, interfaceConfig */
-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;
- var selector = $("#reload_roomname");
- selector.click(function () {
- clearTimeout(updateTimeout);
- clearTimeout(animateTimeout);
- update_roomname();
- });
- selector.show();
-
- update_roomname();
- }
-
- $("#disable_welcome").click(function () {
- window.localStorage.welcomePageDisabled =
- $("#disable_welcome").is(":checked");
- });
-
-}
-
module.exports = setupWelcomePage;
-},{"./RoomnameGenerator":42}],44:[function(require,module,exports){
-/* global $, $iq, config, interfaceConfig */
-var params = {};
-function getConfigParamsFromUrl() {
- if(!location.hash)
- return {};
- var hash = location.hash.substr(1);
- var result = {};
- hash.split("&").forEach(function(part) {
- var item = part.split("=");
- result[item[0]] = JSON.parse(
- decodeURIComponent(item[1]).replace(/\\&/, "&"));
- });
- return result;
-}
-
-params = getConfigParamsFromUrl();
-
-var URLProcessor = {
- setConfigParametersFromUrl: function () {
- for(var key in params) {
- if(typeof key !== "string")
- continue;
-
- var confObj = null, confKey;
- if (key.indexOf("config.") === 0) {
- confObj = config;
- confKey = key.substr("config.".length);
- } else if (key.indexOf("interfaceConfig.") === 0) {
- confObj = interfaceConfig;
- confKey = key.substr("interfaceConfig.".length);
- }
-
- if (!confObj)
- continue;
-
- var value = params[key];
- if (confObj[confKey] && typeof confObj[confKey] !== typeof value)
- {
- console.warn("The type of " + key +
- " is wrong. That parameter won't be updated in config.js.");
- continue;
- }
-
- confObj[confKey] = value;
- }
-
- }
-};
-
-module.exports = URLProcessor;
-},{}],45:[function(require,module,exports){
-/* global APP, require */
-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 statistics 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":162,"../../service/xmpp/XMPPEvents":166,"events":1}],46:[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;
-
-var AdapterJS = require("../RTC/adapter.screenshare");
-
-var EventEmitter = require("events");
-
-var eventEmitter = new EventEmitter();
-
-var DesktopSharingEventTypes
- = require("../../service/desktopsharing/DesktopSharingEventTypes");
-
-var RTCBrowserType = require("../RTC/RTCBrowserType");
-
-var RTCEvents = require("../../service/RTC/RTCEvents");
-
-/**
- * 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 checkChromeExtInstalled(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);
- extInstalled = true;
- // We need to give a moment for the endpoint to become available
- window.setTimeout(function () {
- doGetStreamFromExtension(streamCallback, failCallback);
- }, 500);
- },
- 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) {
-
- obtainDesktopStream = null;
-
- // When TemasysWebRTC plugin is used we always use getUserMedia, so we don't
- // care about 'method' parameter
- if (RTCBrowserType.isTemasysPluginUsed()) {
- if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
- console.info("Screensharing not supported by this plugin version");
- } else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
- console.info(
- "Screensharing not available with Temasys plugin on this site");
- } else {
- obtainDesktopStream = obtainWebRTCScreen;
- console.info("Using Temasys plugin for desktop sharing");
- }
- } else if (RTCBrowserType.isChrome()) {
- if (method == "ext") {
- if (RTCBrowserType.getChromeVersion() >= 34) {
- obtainDesktopStream = obtainScreenFromExtension;
- console.info("Using Chrome extension for desktop sharing");
- initChromeExtension();
- } else {
- console.info("Chrome extension not supported until ver 34");
- }
- } else if (method == "webrtc") {
- obtainDesktopStream = obtainWebRTCScreen;
- console.info("Using Chrome WebRTC for desktop sharing");
- }
- }
-
- if (!obtainDesktopStream) {
- console.info("Desktop sharing disabled");
- }
-}
-
-/**
- * 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 initChromeExtension() {
- // Initialize Chrome extension inline installs
- initInlineInstalls();
- // Check if extension is installed
- checkChromeExtInstalled(function (installed, updateRequired) {
- extInstalled = installed;
- extUpdateRequired = updateRequired;
- console.info(
- "Chrome extension installed: " + extInstalled +
- " updateRequired: " + extUpdateRequired);
- });
-}
-
-function getVideoStreamFailed(error) {
- console.error("Failed to obtain the stream to switch to", error);
- switchInProgress = false;
- isUsingScreenStream = false;
- newStreamCreated(null);
-}
-
-function getDesktopStreamFailed(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);
-}
-
-function onEndedHandler(stream) {
- if (!switchInProgress && isUsingScreenStream) {
- APP.desktopsharing.toggleScreenSharing();
- }
- //FIXME: to be verified
- if (stream.removeEventListener) {
- stream.removeEventListener('ended', onEndedHandler);
- } else {
- stream.detachEvent('ended', onEndedHandler);
- }
-}
-
-// Called when RTC finishes initialization
-function onWebRtcReady() {
-
- setDesktopSharing(config.desktopSharing);
-
- eventEmitter.emit(DesktopSharingEventTypes.INIT);
-}
-
-module.exports = {
- isUsingScreenStream: function () {
- return isUsingScreenStream;
- },
-
- /**
- * @returns {boolean} true if desktop sharing feature is available
- * and enabled.
- */
- isDesktopSharingEnabled: function () { return !!obtainDesktopStream; },
-
- init: function () {
- APP.RTC.addListener(RTCEvents.RTC_READY, onWebRtcReady);
- },
-
- 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
- //FIXME: to be verified
- if (stream.addEventListener) {
- stream.addEventListener('ended', function () {
- onEndedHandler(stream);
- });
- } else {
- stream.attachEvent('ended', function () {
- onEndedHandler(stream);
- });
- }
- newStreamCreated(stream);
- },
- getDesktopStreamFailed);
- } else {
- // Disable screen stream
- APP.RTC.getUserMediaWithConstraints(
- ['video'],
- function (stream) {
- // We are now using camera stream
- isUsingScreenStream = false;
- newStreamCreated(stream);
- },
- getVideoStreamFailed, config.resolution || '360'
- );
- }
- }
-};
-
+},{"./RoomnameGenerator":40}],42:[function(require,module,exports){
+/* global $, $iq, config, interfaceConfig */
+var params = {};
+function getConfigParamsFromUrl() {
+ if(!location.hash)
+ return {};
+ var hash = location.hash.substr(1);
+ var result = {};
+ hash.split("&").forEach(function(part) {
+ var item = part.split("=");
+ result[item[0]] = JSON.parse(
+ decodeURIComponent(item[1]).replace(/\\&/, "&"));
+ });
+ return result;
+}
-},{"../../service/RTC/RTCEvents":157,"../../service/desktopsharing/DesktopSharingEventTypes":163,"../RTC/RTCBrowserType":10,"../RTC/adapter.screenshare":12,"events":1}],47:[function(require,module,exports){
-/* global APP, $ */
-//maps keycode to character, id of popover for given function and function
-var shortcuts = {};
-function initShortcutHandlers() {
- 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 () {
- initShortcutHandlers();
- 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;
+params = getConfigParamsFromUrl();
+
+var URLProcessor = {
+ setConfigParametersFromUrl: function () {
+ for(var key in params) {
+ if(typeof key !== "string")
+ continue;
+
+ var confObj = null, confKey;
+ if (key.indexOf("config.") === 0) {
+ confObj = config;
+ confKey = key.substr("config.".length);
+ } else if (key.indexOf("interfaceConfig.") === 0) {
+ confObj = interfaceConfig;
+ confKey = key.substr("interfaceConfig.".length);
+ }
+
+ if (!confObj)
+ continue;
+
+ var value = params[key];
+ if (confObj[confKey] && typeof confObj[confKey] !== typeof value)
+ {
+ console.warn("The type of " + key +
+ " is wrong. That parameter won't be updated in config.js.");
+ continue;
+ }
+
+ confObj[confKey] = value;
+ }
+
+ }
+};
+
+module.exports = URLProcessor;
+},{}],43:[function(require,module,exports){
+/* global APP, require */
+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 statistics 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":165,"../../service/xmpp/XMPPEvents":169,"events":170}],44:[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;
+
+var AdapterJS = require("../RTC/adapter.screenshare");
+
+var EventEmitter = require("events");
+
+var eventEmitter = new EventEmitter();
+
+var DesktopSharingEventTypes
+ = require("../../service/desktopsharing/DesktopSharingEventTypes");
+
+var RTCBrowserType = require("../RTC/RTCBrowserType");
+
+var RTCEvents = require("../../service/RTC/RTCEvents");
+
+/**
+ * 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 checkChromeExtInstalled(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);
+ extInstalled = true;
+ // We need to give a moment for the endpoint to become available
+ window.setTimeout(function () {
+ doGetStreamFromExtension(streamCallback, failCallback);
+ }, 500);
+ },
+ 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) {
+
+ obtainDesktopStream = null;
+
+ // When TemasysWebRTC plugin is used we always use getUserMedia, so we don't
+ // care about 'method' parameter
+ if (RTCBrowserType.isTemasysPluginUsed()) {
+ if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
+ console.info("Screensharing not supported by this plugin version");
+ } else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
+ console.info(
+ "Screensharing not available with Temasys plugin on this site");
+ } else {
+ obtainDesktopStream = obtainWebRTCScreen;
+ console.info("Using Temasys plugin for desktop sharing");
+ }
+ } else if (RTCBrowserType.isChrome()) {
+ if (method == "ext") {
+ if (RTCBrowserType.getChromeVersion() >= 34) {
+ obtainDesktopStream = obtainScreenFromExtension;
+ console.info("Using Chrome extension for desktop sharing");
+ initChromeExtension();
+ } else {
+ console.info("Chrome extension not supported until ver 34");
+ }
+ } else if (method == "webrtc") {
+ obtainDesktopStream = obtainWebRTCScreen;
+ console.info("Using Chrome WebRTC for desktop sharing");
+ }
+ }
+
+ if (!obtainDesktopStream) {
+ console.info("Desktop sharing disabled");
+ }
+}
+
+/**
+ * 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 initChromeExtension() {
+ // Initialize Chrome extension inline installs
+ initInlineInstalls();
+ // Check if extension is installed
+ checkChromeExtInstalled(function (installed, updateRequired) {
+ extInstalled = installed;
+ extUpdateRequired = updateRequired;
+ console.info(
+ "Chrome extension installed: " + extInstalled +
+ " updateRequired: " + extUpdateRequired);
+ });
+}
+
+function getVideoStreamFailed(error) {
+ console.error("Failed to obtain the stream to switch to", error);
+ switchInProgress = false;
+ isUsingScreenStream = false;
+ newStreamCreated(null);
+}
+
+function getDesktopStreamFailed(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);
+}
+
+function onEndedHandler(stream) {
+ if (!switchInProgress && isUsingScreenStream) {
+ APP.desktopsharing.toggleScreenSharing();
+ }
+ //FIXME: to be verified
+ if (stream.removeEventListener) {
+ stream.removeEventListener('ended', onEndedHandler);
+ } else {
+ stream.detachEvent('ended', onEndedHandler);
+ }
+}
+
+// Called when RTC finishes initialization
+function onWebRtcReady() {
+
+ setDesktopSharing(config.desktopSharing);
+
+ eventEmitter.emit(DesktopSharingEventTypes.INIT);
+}
+
+module.exports = {
+ isUsingScreenStream: function () {
+ return isUsingScreenStream;
+ },
+
+ /**
+ * @returns {boolean} true if desktop sharing feature is available
+ * and enabled.
+ */
+ isDesktopSharingEnabled: function () { return !!obtainDesktopStream; },
+
+ init: function () {
+ APP.RTC.addListener(RTCEvents.RTC_READY, onWebRtcReady);
+ },
+
+ 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
+ //FIXME: to be verified
+ if (stream.addEventListener) {
+ stream.addEventListener('ended', function () {
+ onEndedHandler(stream);
+ });
+ } else {
+ stream.attachEvent('ended', function () {
+ onEndedHandler(stream);
+ });
+ }
+ newStreamCreated(stream);
+ },
+ getDesktopStreamFailed);
+ } else {
+ // Disable screen stream
+ APP.RTC.getUserMediaWithConstraints(
+ ['video'],
+ function (stream) {
+ // We are now using camera stream
+ isUsingScreenStream = false;
+ newStreamCreated(stream);
+ },
+ getVideoStreamFailed, config.resolution || '360'
+ );
+ }
+ }
+};
+
+
+},{"../../service/RTC/RTCEvents":160,"../../service/desktopsharing/DesktopSharingEventTypes":166,"../RTC/RTCBrowserType":8,"../RTC/adapter.screenshare":10,"events":170}],45:[function(require,module,exports){
+/* global APP, $ */
+//maps keycode to character, id of popover for given function and function
+var shortcuts = {};
+function initShortcutHandlers() {
+ 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 () {
+ initShortcutHandlers();
+ 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;
+
+},{}],46:[function(require,module,exports){
+/* global APP, require, $ */
+
+/**
+ * This module is meant to (eventually) contain and manage all information
+ * about members/participants of the conference, so that other modules don't
+ * have to do it on their own, and so that other modules can access members'
+ * information from a single place.
+ *
+ * Currently this module only manages information about the support of jingle
+ * DTMF of the members. Other fields, as well as accessor methods are meant to
+ * be added as needed.
+ */
+
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var Events = require("../../service/members/Events");
+var EventEmitter = require("events");
+
+var eventEmitter = new EventEmitter();
+
+/**
+ * The actual container.
+ */
+var members = {};
+
+/**
+ * There is at least one member that supports DTMF (i.e. is jigasi).
+ */
+var atLeastOneDtmf = false;
+
+
+function registerListeners() {
+ APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, onMucMemberJoined);
+ APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft);
+}
+
+/**
+ * Handles a new member joining the MUC.
+ */
+function onMucMemberJoined(jid, id, displayName) {
+ var member = {
+ displayName: displayName
+ };
+
+ APP.xmpp.getConnection().disco.info(
+ jid, "" /* node */, function(iq) { onDiscoInfoReceived(jid, iq); });
+
+ members[jid] = member;
+}
+
+/**
+ * Handles a member leaving the MUC.
+ */
+function onMucMemberLeft(jid) {
+ delete members[jid];
+ updateAtLeastOneDtmf();
+}
+
+/**
+ * Handles the reception of a disco#info packet from a particular JID.
+ * @param jid the JID sending the packet.
+ * @param iq the packet.
+ */
+function onDiscoInfoReceived(jid, iq) {
+ if (!members[jid])
+ return;
+
+ var supportsDtmf
+ = $(iq).find('>query>feature[var="urn:xmpp:jingle:dtmf:0"]').length > 0;
+ updateDtmf(jid, supportsDtmf);
+}
+
+/**
+ * Updates the 'supportsDtmf' field for a member.
+ * @param jid the jid of the member.
+ * @param newValue the new value for the 'supportsDtmf' field.
+ */
+function updateDtmf(jid, newValue) {
+ var oldValue = members[jid].supportsDtmf;
+ members[jid].supportsDtmf = newValue;
+
+ if (newValue != oldValue) {
+ updateAtLeastOneDtmf();
+ }
+}
+
+/**
+ * Checks each member's 'supportsDtmf' field and updates
+ * 'atLastOneSupportsDtmf'.
+ */
+function updateAtLeastOneDtmf() {
+ var newAtLeastOneDtmf = false;
+ for (var key in members) {
+ if (typeof members[key].supportsDtmf !== 'undefined'
+ && members[key].supportsDtmf) {
+ newAtLeastOneDtmf= true;
+ break;
+ }
+ }
+
+ if (atLeastOneDtmf != newAtLeastOneDtmf) {
+ atLeastOneDtmf = newAtLeastOneDtmf;
+ eventEmitter.emit(Events.DTMF_SUPPORT_CHANGED, atLeastOneDtmf);
+ }
+}
+
+
+/**
+ * Exported interface.
+ */
+var Members = {
+ start: function() {
+ registerListeners();
+ },
+ addListener: function(type, listener) {
+ eventEmitter.on(type, listener);
+ },
+ removeListener: function (type, listener) {
+ eventEmitter.removeListener(type, listener);
+ },
+ size: function () {
+ return Object.keys(members).length;
+ },
+ getMembers: function () {
+ return members;
+ }
+};
+
+module.exports = Members;
+
+},{"../../service/members/Events":167,"../../service/xmpp/XMPPEvents":169,"events":170}],47:[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;
},{}],48:[function(require,module,exports){
-/* global APP, require, $ */
-
-/**
- * This module is meant to (eventually) contain and manage all information
- * about members/participants of the conference, so that other modules don't
- * have to do it on their own, and so that other modules can access members'
- * information from a single place.
- *
- * Currently this module only manages information about the support of jingle
- * DTMF of the members. Other fields, as well as accessor methods are meant to
- * be added as needed.
- */
-
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var Events = require("../../service/members/Events");
-var EventEmitter = require("events");
-
-var eventEmitter = new EventEmitter();
-
-/**
- * The actual container.
- */
-var members = {};
-
-/**
- * There is at least one member that supports DTMF (i.e. is jigasi).
- */
-var atLeastOneDtmf = false;
-
-
-function registerListeners() {
- APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, onMucMemberJoined);
- APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft);
-}
-
-/**
- * Handles a new member joining the MUC.
- */
-function onMucMemberJoined(jid, id, displayName) {
- var member = {
- displayName: displayName
- };
-
- APP.xmpp.getConnection().disco.info(
- jid, "" /* node */, function(iq) { onDiscoInfoReceived(jid, iq); });
-
- members[jid] = member;
-}
-
-/**
- * Handles a member leaving the MUC.
- */
-function onMucMemberLeft(jid) {
- delete members[jid];
- updateAtLeastOneDtmf();
-}
-
-/**
- * Handles the reception of a disco#info packet from a particular JID.
- * @param jid the JID sending the packet.
- * @param iq the packet.
- */
-function onDiscoInfoReceived(jid, iq) {
- if (!members[jid])
- return;
-
- var supportsDtmf
- = $(iq).find('>query>feature[var="urn:xmpp:jingle:dtmf:0"]').length > 0;
- updateDtmf(jid, supportsDtmf);
-}
-
-/**
- * Updates the 'supportsDtmf' field for a member.
- * @param jid the jid of the member.
- * @param newValue the new value for the 'supportsDtmf' field.
- */
-function updateDtmf(jid, newValue) {
- var oldValue = members[jid].supportsDtmf;
- members[jid].supportsDtmf = newValue;
-
- if (newValue != oldValue) {
- updateAtLeastOneDtmf();
- }
-}
-
-/**
- * Checks each member's 'supportsDtmf' field and updates
- * 'atLastOneSupportsDtmf'.
- */
-function updateAtLeastOneDtmf() {
- var newAtLeastOneDtmf = false;
- for (var key in members) {
- if (typeof members[key].supportsDtmf !== 'undefined'
- && members[key].supportsDtmf) {
- newAtLeastOneDtmf= true;
- break;
- }
- }
-
- if (atLeastOneDtmf != newAtLeastOneDtmf) {
- atLeastOneDtmf = newAtLeastOneDtmf;
- eventEmitter.emit(Events.DTMF_SUPPORT_CHANGED, atLeastOneDtmf);
- }
-}
-
-
-/**
- * Exported interface.
- */
-var Members = {
- start: function() {
- registerListeners();
- },
- addListener: function(type, listener) {
- eventEmitter.on(type, listener);
- },
- removeListener: function (type, listener) {
- eventEmitter.removeListener(type, listener);
- },
- size: function () {
- return Object.keys(members).length;
- },
- getMembers: function () {
- return members;
- }
-};
-
-module.exports = Members;
+/* global config, $, APP, Strophe, callstats */
+var jsSHA = require('jssha');
+var io = require('socket.io-client');
+var callStats = null;
-},{"../../service/members/Events":164,"../../service/xmpp/XMPPEvents":166,"events":1}],49:[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;
-
-},{}],50:[function(require,module,exports){
-/* global config, $, APP, Strophe, callstats */
-var jsSHA = require('jssha');
-var io = require('socket.io-client');
-var callStats = null;
-
-function initCallback (err, msg) {
- console.log("Initializing Status: err="+err+" msg="+msg);
-}
-
-var CallStats = {
- init: function (jingleSession) {
-
- if(!config.callStatsID || !config.callStatsSecret || callStats !== null)
- return;
-
- callStats = new callstats($, io, jsSHA);
-
- this.session = jingleSession;
- this.peerconnection = jingleSession.peerconnection.peerconnection;
-
- this.userID = APP.xmpp.myResource();
-
- var location = window.location;
- this.confID = location.protocol + "//" +
- location.hostname + location.pathname;
-
- //userID is generated or given by the origin server
- callStats.initialize(config.callStatsID,
- config.callStatsSecret,
- this.userID,
- initCallback);
-
- var usage = callStats.fabricUsage.multiplex;
-
- callStats.addNewFabric(this.peerconnection,
- Strophe.getResourceFromJid(jingleSession.peerjid),
- usage,
- this.confID,
- this.pcCallback.bind(this));
- },
- pcCallback: function (err, msg) {
- if (!callStats)
- return;
- console.log("Monitoring status: "+ err + " msg: " + msg);
- callStats.sendFabricEvent(this.peerconnection,
- callStats.fabricEvent.fabricSetup, this.confID);
- },
- sendMuteEvent: function (mute, type) {
- if (!callStats)
- return;
- var event = null;
- if (type === "video") {
- event = (mute? callStats.fabricEvent.videoPause :
- callStats.fabricEvent.videoResume);
- }
- else {
- event = (mute? callStats.fabricEvent.audioMute :
- callStats.fabricEvent.audioUnmute);
- }
- callStats.sendFabricEvent(this.peerconnection, event, this.confID);
- },
- sendTerminateEvent: function () {
- if(!callStats) {
- return;
- }
- callStats.sendFabricEvent(this.peerconnection,
- callStats.fabricEvent.fabricTerminated, this.confID);
- },
- sendSetupFailedEvent: function () {
- if(!callStats) {
- return;
- }
- callStats.sendFabricEvent(this.peerconnection,
- callStats.fabricEvent.fabricSetupFailed, this.confID);
- }
-
-};
-module.exports = CallStats;
-},{"jssha":73,"socket.io-client":103}],51:[function(require,module,exports){
-/* global config */
-/**
- * Provides statistics for the local stream.
- */
-
-var RTCBrowserType = require('../RTC/RTCBrowserType');
-
-/**
- * Size of the webaudio analyzer buffer.
- * @type {number}
- */
-var WEBAUDIO_ANALYZER_FFT_SIZE = 2048;
-
-/**
- * Value of the webaudio analyzer smoothing time parameter.
- * @type {number}
- */
-var WEBAUDIO_ANALYZER_SMOOTING_TIME = 0.8;
-
-/**
- * Converts time domain data array to audio level.
- * @param samples 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.
- * @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 ||
- RTCBrowserType.isTemasysPluginUsed())
- return;
-
- var context = new AudioContext();
- var analyser = context.createAnalyser();
- analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
- analyser.fftSize = WEBAUDIO_ANALYZER_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;
-},{"../RTC/RTCBrowserType":10}],52:[function(require,module,exports){
-/* global require, ssrc2jid */
-/* jshint -W117 */
-var RTCBrowserType = require("../RTC/RTCBrowserType");
-
-/* Whether we support the browser we are running into for logging statistics */
-var browserSupported = RTCBrowserType.isChrome() ||
- RTCBrowserType.isOpera();
-/**
- * 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) {
- var browserType = RTCBrowserType.getBrowserType();
- if (!keyMap[browserType][name])
- throw "The property isn't supported!";
- var key = keyMap[browserType][name];
- return (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) ?
- 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 && browserSupported) {
- 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 && browserSupported) {
- 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"
-};
-keyMap[RTCBrowserType.RTC_BROWSER_OPERA] =
- keyMap[RTCBrowserType.RTC_BROWSER_CHROME];
-
-
-/**
- * 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) {
- if((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);
- }
- }
-};
-
-},{"../RTC/RTCBrowserType":10}],53:[function(require,module,exports){
-/* global require, APP */
-/**
- * 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 CallStats = require("./CallStats");
-var RTCEvents = require("../../service/RTC/RTCEvents");
-
-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 = 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) {
- CallStats.sendTerminateEvent();
- 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);
- //FIXME: we may want to change CALL INCOMING event to onnegotiationneeded
- APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
- startRemoteStats(event.peerconnection);
-// CallStats.init(event);
- });
- APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY, function (session) {
- CallStats.init(session);
- });
- APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) {
- CallStats.sendMuteEvent(mute, "audio");
- });
- APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
- CallStats.sendSetupFailedEvent();
- });
- APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) {
- CallStats.sendMuteEvent(mute, "video");
- });
- }
-};
-
-
-
-
-module.exports = statistics;
-},{"../../service/RTC/RTCEvents":157,"../../service/RTC/StreamEventTypes.js":159,"../../service/xmpp/XMPPEvents":166,"./CallStats":50,"./LocalStatsCollector.js":51,"./RTPStatsCollector.js":52,"events":1}],54:[function(require,module,exports){
-/* global $, require, config, interfaceConfig */
-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,
- 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":165,"../settings/Settings":49,"i18next-client":72}],55:[function(require,module,exports){
-/*
- * JingleSession provides an API to manage a single Jingle session. We will
- * have different implementations depending on the underlying interface used
- * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
- */
-function JingleSession(me, sid, connection, service, eventEmitter) {
- /**
- * Our JID.
- */
- this.me = me;
-
- /**
- * The Jingle session identifier.
- */
- this.sid = sid;
-
- /**
- * The XMPP connection.
- */
- this.connection = connection;
-
- /**
- * The XMPP service.
- */
- this.service = service;
-
- /**
- * The event emitter.
- */
- this.eventEmitter = eventEmitter;
-
- /**
- * Whether to use dripping or not. Dripping is sending trickle candidates
- * not one-by-one.
- * Note: currently we do not support 'false'.
- */
- this.usedrip = true;
-
- /**
- * When dripping is used, stores ICE candidates which are to be sent.
- */
- this.drip_container = [];
-
- // Media constraints. Is this WebRTC only?
- this.media_constraints = null;
-
- // ICE servers config (RTCConfiguration?).
- this.ice_config = {};
-}
-
-/**
- * Prepares this object to initiate a session.
- * @param peerjid the JID of the remote peer.
- * @param isInitiator whether we will be the Jingle initiator.
- * @param media_constraints
- * @param ice_config
- */
-JingleSession.prototype.initialize = function(peerjid, isInitiator,
- media_constraints, ice_config) {
- this.media_constraints = media_constraints;
- this.ice_config = ice_config;
-
- if (this.state !== null) {
- console.error('attempt to initiate on session ' + this.sid +
- 'in state ' + this.state);
- return;
- }
- this.state = 'pending';
- this.initiator = isInitiator ? this.me : peerjid;
- this.responder = !isInitiator ? this.me : peerjid;
- this.peerjid = peerjid;
-
- this.doInitialize();
-};
-
-/**
- * Finishes initialization.
- */
-JingleSession.prototype.doInitialize = function() {};
-
-/**
- * Adds the ICE candidates found in the 'contents' array as remote candidates?
- * Note: currently only used on transport-info
- */
-JingleSession.prototype.addIceCandidates = function(contents) {};
-
-/**
- * Handles an 'add-source' event.
- *
- * @param contents an array of Jingle 'content' elements.
- */
-JingleSession.prototype.addSources = function(contents) {};
-
-/**
- * Handles a 'remove-source' event.
- *
- * @param contents an array of Jingle 'content' elements.
- */
-JingleSession.prototype.removeSources = function(contents) {};
-
-/**
- * Terminates this Jingle session (stops sending media and closes the streams?)
- */
-JingleSession.prototype.terminate = function() {};
-
-/**
- * Sends a Jingle session-terminate message to the peer and terminates the
- * session.
- * @param reason
- * @param text
- */
-JingleSession.prototype.sendTerminate = function(reason, text) {};
-
-/**
- * Handles an offer from the remote peer (prepares to accept a session).
- * @param jingle the 'jingle' XML element.
- */
-JingleSession.prototype.setOffer = function(jingle) {};
-
-/**
- * Handles an answer from the remote peer (prepares to accept a session).
- * @param jingle the 'jingle' XML element.
- */
-JingleSession.prototype.setAnswer = function(jingle) {};
-
-
-module.exports = JingleSession;
-
-},{}],56:[function(require,module,exports){
-/* jshint -W117 */
-var JingleSession = require("./JingleSession");
-var TraceablePeerConnection = require("./TraceablePeerConnection");
-var SDPDiffer = require("./SDPDiffer");
-var SDPUtil = require("./SDPUtil");
-var SDP = require("./SDP");
-var async = require("async");
-var transform = require("sdp-transform");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var RTCBrowserType = require("../RTC/RTCBrowserType");
-var SSRCReplacement = require("./LocalSSRCReplacement");
-
-// Jingle stuff
-function JingleSessionPC(me, sid, connection, service, eventEmitter) {
- JingleSession.call(this, me, sid, connection, service, eventEmitter);
- this.initiator = null;
- this.responder = null;
- this.peerjid = null;
- this.state = null;
- this.localSDP = null;
- this.remoteSDP = null;
- this.relayedStreams = [];
- this.pc_constraints = null;
-
- this.usetrickle = true;
- this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
-
- 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;
- this.ssrcOwners = {};
- this.ssrcVideoTypes = {};
- this.eventEmitter = eventEmitter;
-
- /**
- * 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;
- this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1);
- // We start with the queue paused. We resume it when the signaling state is
- // stable and the ice connection state is connected.
- this.modifySourcesQueue.pause();
-}
-JingleSessionPC.prototype = JingleSession.prototype;
-JingleSessionPC.prototype.constructor = JingleSessionPC;
-
-
-JingleSessionPC.prototype.setOffer = function(offer) {
- this.setRemoteDescription(offer, 'offer');
-};
-
-JingleSessionPC.prototype.setAnswer = function(answer) {
- this.setRemoteDescription(answer, 'answer');
-};
-
-JingleSessionPC.prototype.updateModifySourcesQueue = function() {
- var signalingState = this.peerconnection.signalingState;
- var iceConnectionState = this.peerconnection.iceConnectionState;
- if (signalingState === 'stable' && iceConnectionState === 'connected') {
- this.modifySourcesQueue.resume();
- } else {
- this.modifySourcesQueue.pause();
- }
-};
-
-JingleSessionPC.prototype.doInitialize = function () {
- var self = this;
-
- this.hadstuncandidate = false;
- this.hadturncandidate = false;
- this.lasticecandidate = false;
- this.isreconnect = false;
-
- this.peerconnection = new TraceablePeerConnection(
- this.connection.jingle.ice_config,
- this.connection.jingle.pc_constraints,
- this);
-
- this.peerconnection.onicecandidate = function (event) {
- self.sendIceCandidate(event.candidate);
- };
- this.peerconnection.onaddstream = function (event) {
- if (event.stream.id !== 'default') {
- console.log("REMOTE STREAM ADDED: ", event.stream , event.stream.id);
- self.remoteStreamAdded(event);
- } else {
- // This is a recvonly stream. Clients that implement Unified Plan,
- // such as Firefox use recvonly "streams/channels/tracks" for
- // receiving remote stream/tracks, as opposed to Plan B where there
- // are only 3 channels: audio, video and data.
- console.log("RECVONLY REMOTE STREAM IGNORED: " + event.stream + " - " + event.stream.id);
- }
- };
- 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;
- self.updateModifySourcesQueue();
- };
- /**
- * The oniceconnectionstatechange event handler contains the code to execute when the iceconnectionstatechange event,
- * of type Event, is received by this RTCPeerConnection. Such an event is sent when the value of
- * RTCPeerConnection.iceConnectionState changes.
- *
- * @param event the event containing information about the change
- */
- this.peerconnection.oniceconnectionstatechange = function (event) {
- if (!(self && self.peerconnection)) return;
- self.updateModifySourcesQueue();
- switch (self.peerconnection.iceConnectionState) {
- case 'connected':
-
- // Informs interested parties that the connection has been restored.
- if (self.peerconnection.signalingState === 'stable' && self.isreconnect)
- self.eventEmitter.emit(XMPPEvents.CONNECTION_RESTORED);
- self.isreconnect = false;
-
- break;
- case 'disconnected':
- self.isreconnect = true;
- // Informs interested parties that the connection has been interrupted.
- if (self.peerconnection.signalingState === 'stable')
- self.eventEmitter.emit(XMPPEvents.CONNECTION_INTERRUPTED);
- break;
- case 'failed':
- self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- break;
- }
- onIceConnectionStateChange(self.sid, self);
- };
- this.peerconnection.onnegotiationneeded = function (event) {
- self.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, 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;
- }
-}
-
-JingleSessionPC.prototype.accept = function () {
- 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');
- }
- 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 });
- // FIXME why do we generate session-accept in 3 different places ?
- 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();
-
- SSRCReplacement.processSessionInit(accept);
-
- 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';
- JingleSessionPC.onJingleError(self.sid, error);
- },
- 10000);
- },
- function (e) {
- console.error('setLocalDescription failed', e);
- self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- }
- );
-};
-
-JingleSessionPC.prototype.terminate = function (reason) {
- this.state = 'ended';
- this.reason = reason;
- this.peerconnection.close();
- if (this.statsinterval !== null) {
- window.clearInterval(this.statsinterval);
- this.statsinterval = null;
- }
-};
-
-JingleSessionPC.prototype.active = function () {
- return this.state == 'active';
-};
-
-JingleSessionPC.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...');
- //FIXME why do we generate session-accept in 3 different places ?
- 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 sendJingle = function (ssrc) {
- if(!ssrc)
- ssrc = {};
- self.localSDP.toJingle(
- init,
- self.initiator == self.me ? 'initiator' : 'responder',
- ssrc);
-
- SSRCReplacement.processSessionInit(init);
-
- 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';
- JingleSessionPC.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]);
- }
- }
-};
-
-JingleSessionPC.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';
- JingleSessionPC.onJingleError(this.sid, error);
- },
- 10000);
-};
-
-
-JingleSessionPC.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
- );
-};
-
-// FIXME createdOffer is never used in jitsi-meet
-JingleSessionPC.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);
-
- SSRCReplacement.processSessionInit(init);
-
- 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';
- JingleSessionPC.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);
- self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- }
- );
- 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;
- }
- }
-};
-
-JingleSessionPC.prototype.readSsrcInfo = function (contents) {
- var self = this;
- $(contents).each(function (idx, content) {
- var name = $(content).attr('name');
- var mediaType = this.getAttribute('name');
- var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
- ssrcs.each(function () {
- var ssrc = this.getAttribute('ssrc');
- $(this).find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]').each(
- function () {
- var owner = this.getAttribute('owner');
- self.ssrcOwners[ssrc] = owner;
- }
- );
- });
- });
-};
-
-JingleSessionPC.prototype.getSsrcOwner = function (ssrc) {
- return this.ssrcOwners[ssrc];
-};
-
-JingleSessionPC.prototype.setRemoteDescription = function (elem, desctype) {
- //console.log('setting remote description... ', desctype);
- this.remoteSDP = new SDP('');
- this.remoteSDP.fromJingle(elem);
- this.readSsrcInfo($(elem).find(">content"));
- 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);
- JingleSessionPC.onJingleFatalError(self, e);
- }
- );
-};
-
-JingleSessionPC.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);
- }
- });
- });
-};
-
-JingleSessionPC.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);
- self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- },
- this.media_constraints
- );
-};
-
-JingleSessionPC.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) {
- // FIXME why do we generate session-accept in 3 different places ?
- 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 });
- self.localSDP.toJingle(
- accept,
- self.initiator == self.me ? 'initiator' : 'responder',
- ssrcs);
-
- SSRCReplacement.processSessionInit(accept);
-
- 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';
- JingleSessionPC.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);
- self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- }
- );
- 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;
- }
- }
-};
-
-JingleSessionPC.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;
- }
-};
-
-JingleSessionPC.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);
-
- this.readSsrcInfo(elem);
-
- 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;
- }
- if (sdp.containsSSRC(ssrc)) {
- console.warn("Source-add request for existing 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.modifySourcesQueue.push(function() {
- // When a source is added and if this is FF, a new channel is allocated
- // for receiving the added source. We need to diffuse the SSRC of this
- // new recvonly channel to the rest of the peers.
- console.log('modify sources done');
-
- var newSdp = new SDP(self.peerconnection.localDescription.sdp);
- console.log("SDPs", mySdp, newSdp);
- self.notifyMySSRCUpdate(mySdp, newSdp);
- });
-};
-
-JingleSessionPC.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.modifySourcesQueue.push(function() {
- // When a source is removed and if this is FF, the recvonly channel that
- // receives the remote stream is deactivated . We need to diffuse the
- // recvonly SSRC removal to the rest of the peers.
- console.log('modify sources done');
-
- var newSdp = new SDP(self.peerconnection.localDescription.sdp);
- console.log("SDPs", mySdp, newSdp);
- self.notifyMySSRCUpdate(mySdp, newSdp);
- });
-};
-
-JingleSessionPC.prototype._modifySources = function (successCallback, queueCallback) {
- 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();
- }
- queueCallback();
- 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 = [];
-
- 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");
- queueCallback("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();
- }
- queueCallback();
- },
- function(error) {
- console.error('modified setLocalDescription failed', error);
- queueCallback(error);
- }
- );
- },
- function(error) {
- console.error('modified answer failed', error);
- queueCallback(error);
- }
- );
- },
- function(error) {
- console.error('modify failed', error);
- queueCallback(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.
- */
-JingleSessionPC.prototype.switchStreams = function (new_stream, oldStream, success_callback, isAudio) {
-
- 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);
- if(new_stream)
- self.peerconnection.addStream(new_stream);
- }
-
- // Conference is not active
- if(!oldSdp || !self.peerconnection) {
- success_callback();
- return;
- }
-
- self.switchstreams = true;
- self.modifySourcesQueue.push(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.
- */
-JingleSessionPC.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);
-
- // Let 'source-remove' IQ through the hack and see if we're allowed to send
- // it in the current form
- if (removed)
- remove = SSRCReplacement.processSourceRemove(remove);
-
- if (removed && remove) {
- console.info("Sending source-remove", remove);
- 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);
-
- // Let 'source-add' IQ through the hack and see if we're allowed to send
- // it in the current form
- if (added)
- add = SSRCReplacement.processSourceAdd(add);
-
- if (added && add) {
- console.info("Sending source-add", add);
- 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');
- }
-};
-
-/**
- * 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)
- */
-JingleSessionPC.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);
-
- var self = this;
- var oldSdp = null;
- if(self.peerconnection) {
- if(self.peerconnection.localDescription) {
- oldSdp = new SDP(self.peerconnection.localDescription.sdp);
- }
- }
-
- this.modifySourcesQueue.push(function() {
- console.log('modify sources done');
-
- callback(mute);
-
- var newSdp = new SDP(self.peerconnection.localDescription.sdp);
- console.log("SDPs", oldSdp, newSdp);
- self.notifyMySSRCUpdate(oldSdp, newSdp);
- });
-};
-
-JingleSessionPC.prototype.hardMuteVideo = function (muted) {
- this.pendingop = muted ? 'mute' : 'unmute';
-};
-
-JingleSessionPC.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);
-};
-
-JingleSessionPC.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);
-};
-
-JingleSessionPC.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;
-};
-
-JingleSessionPC.onJingleError = function (session, error)
-{
- console.error("Jingle error", error);
-}
-
-JingleSessionPC.onJingleFatalError = function (session, error)
-{
- this.service.sessionTerminated = true;
- this.connection.emuc.doLeave();
- this.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
- this.eventEmitter.emit(XMPPEvents.JINGLE_FATAL_ERROR, session, error);
-}
-
-JingleSessionPC.prototype.setLocalDescription = function () {
- var self = this;
- var newssrcs = [];
- var session = transform.parse(this.peerconnection.localDescription.sdp);
- session.media.forEach(function (media) {
-
- if (media.ssrcs != null && media.ssrcs.length > 0) {
- // TODO(gp) maybe exclude FID streams?
- media.ssrcs.forEach(function (ssrc) {
- if (ssrc.attribute !== 'cname') {
- return;
- }
- newssrcs.push({
- 'ssrc': ssrc.id,
- 'type': media.type
- });
- });
- }
- else if(self.localStreamsSSRC && self.localStreamsSSRC[media.type])
- {
- newssrcs.push({
- 'ssrc': self.localStreamsSSRC[media.type],
- 'type': media.type
- });
- }
-
- });
-
- console.log('new ssrcs', newssrcs);
-
- // Bind us as local SSRCs owner
- if (newssrcs.length > 0) {
- for (var i = 1; i <= newssrcs.length; i ++) {
- var ssrc = newssrcs[i-1].ssrc;
- var myJid = self.connection.emuc.myroomjid;
- self.ssrcOwners[ssrc] = myJid;
- }
- }
-}
-
-// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
-// TODO: is this hack (along with the XMPPEvent-s used only for it) still needed
-// now that we announce an SSRC for receive-only streams?
-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);
- eventEmitter.emit(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR);
- }
- );
- },
- function (error) {
- console.log('triggerKeyframe createAnswer failed', error);
- eventEmitter.emit(XMPPEvents.CREATE_ANSWER_ERROR);
- }
- );
- },
- function (error) {
- console.log('triggerKeyframe setRemoteDescription failed', error);
- eventEmitter.emit(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR);
- }
- );
-}
-
-
-JingleSessionPC.prototype.remoteStreamAdded = function (data, times) {
- var self = this;
- var thessrc;
- var streamId = APP.RTC.getStreamID(data.stream);
-
- // look up an associated JID for a stream id
- if (!streamId) {
- console.error("No stream ID for", data.stream);
- } else if (streamId && streamId.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;
-
- if (RTCBrowserType.isTemasysPluginUsed()) {
- return ((line.indexOf('mslabel:' + streamId) !== -1));
- } else {
- return ((line.indexOf('msid:' + streamId) !== -1));
- }
- });
- if (ssrclines.length) {
- thessrc = ssrclines[0].substring(7).split(' ')[0];
-
- if (!self.ssrcOwners[thessrc]) {
- console.error("No SSRC owner known for: " + thessrc);
- return;
- }
- data.peerjid = self.ssrcOwners[thessrc];
- console.log('associated jid', self.ssrcOwners[thessrc]);
- } else {
- console.error("No SSRC lines for ", streamId);
- }
- }
-
- 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
- // TODO: is this hack still needed now that we announce an SSRC for
- // receive-only streams?
- 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 = JingleSessionPC;
-
-},{"../../service/xmpp/XMPPEvents":166,"../RTC/RTCBrowserType":10,"./JingleSession":55,"./LocalSSRCReplacement":57,"./SDP":58,"./SDPDiffer":59,"./SDPUtil":60,"./TraceablePeerConnection":61,"async":71,"sdp-transform":100}],57:[function(require,module,exports){
-/* global $ */
-
-/*
- Here we do modifications of local video SSRCs. There are 2 situations we have
- to handle:
-
- 1. We generate SSRC for local recvonly video stream. This is the case when we
- have no local camera and it is not generated automatically, but SSRC=1 is
- used implicitly. If that happens RTCP packets will be dropped by the JVB
- and we won't be able to request video key frames correctly.
-
- 2. A hack to re-use SSRC of the first video stream for any new stream created
- in future. It turned out that Chrome may keep on using the SSRC of removed
- video stream in RTCP even though a new one has been created. So we just
- want to avoid that by re-using it. Jingle 'source-remove'/'source-add'
- notifications are blocked once first video SSRC is advertised to the focus.
-
- What this hack does:
-
- 1. Stores the SSRC of the first video stream created by
- a) scanning Jingle session-accept/session-invite for existing video SSRC
- b) watching for 'source-add' for new video stream if it has not been
- created in step a)
- 2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with
- the stored one. It is called by 'TracablePeerConnection' before local SDP is
- returned to the other parts of the application.
- 3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and
- blocks those notifications. This makes Jicofo and all participants think
- that it exists all the time even if the video stream has been removed or
- replaced locally. Thanks to that there is no additional signaling activity
- on video mute or when switching to the desktop stream.
- */
-
-var SDP = require('./SDP');
-var RTCBrowserType = require('../RTC/RTCBrowserType');
-
-/**
- * The hack is enabled on all browsers except FF by default
- * FIXME finish the hack once removeStream method is implemented in FF
- * @type {boolean}
- */
-var isEnabled = !RTCBrowserType.isFirefox();
-
-/**
- * Stored SSRC of local video stream.
- */
-var localVideoSSRC;
-
-/**
- * SSRC used for recvonly video stream when we have no local camera.
- * This is in order to tell Chrome what SSRC should be used in RTCP requests
- * instead of 1.
- */
-var localRecvOnlySSRC;
-
-/**
- * cname for localRecvOnlySSRC
- */
-var localRecvOnlyCName;
-
-/**
- * Method removes element which describes localVideoSSRC
- * from given Jingle IQ.
- * @param modifyIq 'source-add' or 'source-remove' Jingle IQ.
- * @param actionName display name of the action which will be printed in log
- * messages.
- * @returns {*} modified Jingle IQ, so that it does not contain element
- * corresponding to localVideoSSRC or null if no
- * other SSRCs left to be signaled after removing it.
- */
-var filterOutSource = function (modifyIq, actionName) {
- var modifyIqTree = $(modifyIq.tree());
-
- if (!localVideoSSRC)
- return modifyIqTree[0];
-
- var videoSSRC = modifyIqTree.find(
- '>jingle>content[name="video"]' +
- '>description>source[ssrc="' + localVideoSSRC + '"]');
-
- if (!videoSSRC.length) {
- return modifyIqTree[0];
- }
-
- console.info(
- 'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC);
-
- videoSSRC.remove();
-
- // Check if any sources still left to be added/removed
- if (modifyIqTree.find('>jingle>content>description>source').length) {
- return modifyIqTree[0];
- } else {
- return null;
- }
-};
-
-/**
- * Scans given Jingle IQ for video SSRC and stores it.
- * @param jingleIq the Jingle IQ to be scanned for video SSRC.
- */
-var storeLocalVideoSSRC = function (jingleIq) {
- var videoSSRCs =
- $(jingleIq.tree())
- .find('>jingle>content[name="video"]>description>source');
-
- videoSSRCs.each(function (idx, ssrcElem) {
- if (localVideoSSRC)
- return;
- // We consider SSRC real only if it has msid attribute
- // recvonly streams in FF do not have it as well as local SSRCs
- // we generate for recvonly streams in Chrome
- var ssrSel = $(ssrcElem);
- var msid = ssrSel.find('>parameter[name="msid"]');
- if (msid.length) {
- var ssrcVal = ssrSel.attr('ssrc');
- if (ssrcVal) {
- localVideoSSRC = ssrcVal;
- console.info('Stored local video SSRC' +
- ' for future re-use: ' + localVideoSSRC);
- }
- }
- });
-};
-
-/**
- * Generates new SSRC for local video recvonly stream.
- * FIXME what about eventual SSRC collision ?
- */
-function generateRecvonlySSRC() {
- //
- localRecvOnlySSRC =
- Math.random().toString(10).substring(2, 11);
- localRecvOnlyCName =
- Math.random().toString(36).substring(2);
- console.info(
- "Generated local recvonly SSRC: " + localRecvOnlySSRC +
- ", cname: " + localRecvOnlyCName);
-}
-
-var LocalSSRCReplacement = {
- /**
- * Method must be called before 'session-initiate' or 'session-invite' is
- * sent. Scans the IQ for local video SSRC and stores it if detected.
- *
- * @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ
- * which will be scanned for local video SSRC.
- */
- processSessionInit: function (sessionInit) {
- if (!isEnabled)
- return;
-
- if (localVideoSSRC) {
- console.error("Local SSRC stored already: " + localVideoSSRC);
- return;
- }
- storeLocalVideoSSRC(sessionInit);
- },
- /**
- * If we have local video SSRC stored searched given
- * localDescription for video SSRC and makes sure it is replaced
- * with the stored one.
- * @param localDescription local description object that will have local
- * video SSRC replaced with the stored one
- * @returns modified localDescription object.
- */
- mungeLocalVideoSSRC: function (localDescription) {
- if (!isEnabled)
- return localDescription;
-
- if (!localDescription) {
- console.warn("localDescription is null or undefined");
- return localDescription;
- }
-
- // IF we have local video SSRC stored make sure it is replaced
- // with old SSRC
- if (localVideoSSRC) {
- var newSdp = new SDP(localDescription.sdp);
- if (newSdp.media[1].indexOf("a=ssrc:") !== -1 &&
- !newSdp.containsSSRC(localVideoSSRC)) {
- // Get new video SSRC
- var map = newSdp.getMediaSsrcMap();
- var videoPart = map[1];
- var videoSSRCs = videoPart.ssrcs;
- var newSSRC = Object.keys(videoSSRCs)[0];
-
- console.info(
- "Replacing new video SSRC: " + newSSRC +
- " with " + localVideoSSRC);
-
- localDescription.sdp =
- newSdp.raw.replace(
- new RegExp('a=ssrc:' + newSSRC, 'g'),
- 'a=ssrc:' + localVideoSSRC);
- }
- } else {
- // Make sure we have any SSRC for recvonly video stream
- var sdp = new SDP(localDescription.sdp);
-
- if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 &&
- sdp.media[1].indexOf('a=recvonly') !== -1) {
-
- if (!localRecvOnlySSRC) {
- generateRecvonlySSRC();
- }
-
- console.info('No SSRC in video recvonly stream' +
- ' - adding SSRC: ' + localRecvOnlySSRC);
-
- sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC +
- ' cname:' + localRecvOnlyCName + '\r\n';
-
- localDescription.sdp = sdp.session + sdp.media.join('');
- }
- }
- return localDescription;
- },
- /**
- * Method must be called before 'source-add' notification is sent. In case
- * we have local video SSRC advertised already it will be removed from the
- * notification. If no other SSRCs are described by given IQ null will be
- * returned which means that there is no point in sending the notification.
- * @param sourceAdd 'source-add' Jingle IQ to be processed
- * @returns modified 'source-add' IQ which can be sent to the focus or
- * null if no notification shall be sent. It is no longer
- * a Strophe IQ Builder instance, but DOM element tree.
- */
- processSourceAdd: function (sourceAdd) {
- if (!isEnabled)
- return sourceAdd;
-
- if (!localVideoSSRC) {
- // Store local SSRC if available
- storeLocalVideoSSRC(sourceAdd);
- return sourceAdd;
- } else {
- return filterOutSource(sourceAdd, 'source-add');
- }
- },
- /**
- * Method must be called before 'source-remove' notification is sent.
- * Removes local video SSRC from the notification. If there are no other
- * SSRCs described in the given IQ null will be returned which
- * means that there is no point in sending the notification.
- * @param sourceRemove 'source-remove' Jingle IQ to be processed
- * @returns modified 'source-remove' IQ which can be sent to the focus or
- * null if no notification shall be sent. It is no longer
- * a Strophe IQ Builder instance, but DOM element tree.
- */
- processSourceRemove: function (sourceRemove) {
- if (!isEnabled)
- return sourceRemove;
-
- return filterOutSource(sourceRemove, 'source-remove');
- },
-
- /**
- * Turns the hack on or off
- * @param enabled true to enable the hack or false
- * to disable it
- */
- setEnabled: function (enabled) {
- isEnabled = enabled;
- }
-};
-
-module.exports = LocalSSRCReplacement;
-
-},{"../RTC/RTCBrowserType":10,"./SDP":58}],58:[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 {
- var k = kv.split(':', 2)[0];
- elem.attrs({ name: k });
-
- var v = kv.split(':', 2)[1];
- v = SDPUtil.filter_special_chars(v);
- elem.attrs({ value: v });
- }
- 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 = SDPUtil.filter_special_chars(msid);
- 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 () {
- var name = this.getAttribute('name');
- var value = this.getAttribute('value');
- value = SDPUtil.filter_special_chars(value);
- media += 'a=ssrc:' + ssrc + ' ' + name;
- if (value && value.length)
- media += ':' + value;
- media += '\r\n';
- });
- });
-
- return media;
-};
-
-
-module.exports = SDP;
-
-
-},{"./SDPUtil":60}],59:[function(require,module,exports){
-
-var SDPUtil = require("./SDPUtil");
-
-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 {
- var nv = kv.split(':', 2);
- var name = nv[0];
- var value = SDPUtil.filter_special_chars(nv[1]);
- modify.attrs({ name: name });
- modify.attrs({ value: value });
- }
- 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;
-},{"./SDPUtil":60}],60:[function(require,module,exports){
-SDPUtil = {
- filter_special_chars: function (text) {
- return text.replace(/[\\\/\{,\}\+]/g, "");
- },
- 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;
-},{}],61:[function(require,module,exports){
-var RTC = require('../RTC/RTC');
-var RTCBrowserType = require("../RTC/RTCBrowserType.js");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var SSRCReplacement = require("./LocalSSRCReplacement");
-
-function TraceablePeerConnection(ice_config, constraints, session) {
- var self = this;
- var RTCPeerConnectionType = null;
- if (RTCBrowserType.isFirefox()) {
- RTCPeerConnectionType = mozRTCPeerConnection;
- } else if (RTCBrowserType.isTemasysPluginUsed()) {
- RTCPeerConnectionType = RTCPeerConnection;
- } else {
- RTCPeerConnectionType = webkitRTCPeerConnection;
- }
- this.peerconnection = new RTCPeerConnectionType(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();
- var Simulcast = require('sdp-simulcast');
- this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
-
- // override as desired
- this.trace = function (what, info) {
- /*console.warn('WTRACE', what, info);
- if (info && RTCBrowserType.isIExplorer()) {
- if (info.length > 1024) {
- console.warn('WTRACE', what, info.substr(1024));
- }
- if (info.length > 2048) {
- console.warn('WTRACE', what, info.substr(2048));
- }
- }*/
- self.updateLog.push({
- time: new Date(),
- type: what,
- value: info || ""
- });
- };
- this.onicecandidate = null;
- this.peerconnection.onicecandidate = function (event) {
- // FIXME: this causes stack overflow with Temasys Plugin
- if (!RTCBrowserType.isTemasysPluginUsed())
- 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);
- }
- };
- // XXX: do all non-firefox browsers which we support also support this?
- if (!RTCBrowserType.isFirefox() && this.maxstats) {
- this.statsinterval = window.setInterval(function() {
- self.peerconnection.getStats(function(stats) {
- var results = stats.result();
- var now = new Date();
- for (var i = 0; i < results.length; ++i) {
- 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);
- }
-}
-
-/**
- * Returns a string representation of a SessionDescription object.
- */
-var dumpSDP = function(description) {
- if (typeof description === 'undefined' || description == null) {
- return '';
- }
-
- return 'type: ' + description.type + '\r\n' + description.sdp;
-};
-
-/**
- * Takes a SessionDescription object and returns a "normalized" version.
- * Currently it only takes care of ordering the a=ssrc lines.
- */
-var normalizePlanB = function(desc) {
- if (typeof desc !== 'object' || desc === null ||
- typeof desc.sdp !== 'string') {
- console.warn('An empty description was passed as an argument.');
- return desc;
- }
-
- var transform = require('sdp-transform');
- var session = transform.parse(desc.sdp);
-
- if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
- Array.isArray(session.media)) {
- session.media.forEach(function (mLine) {
-
- // Chrome appears to be picky about the order in which a=ssrc lines
- // are listed in an m-line when rtx is enabled (and thus there are
- // a=ssrc-group lines with FID semantics). Specifically if we have
- // "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
- // the "a=ssrc:S1" lines, SRD fails.
- // So, put SSRC which appear as the first SSRC in an FID ssrc-group
- // first.
- var firstSsrcs = [];
- var newSsrcLines = [];
-
- if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
- mLine.ssrcGroups.forEach(function (group) {
- if (typeof group.semantics !== 'undefined' &&
- group.semantics === 'FID') {
- if (typeof group.ssrcs !== 'undefined') {
- firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
- }
- }
- });
- }
-
- if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
- for (var i = 0; i 0) {
- // start gathering stats
- }
- */
-};
-
-TraceablePeerConnection.prototype.setRemoteDescription
- = function (description, successCallback, failureCallback) {
- this.trace('setRemoteDescription::preTransform', dumpSDP(description));
- // TODO the focus should squeze or explode the remote simulcast
- description = this.simulcast.mungeRemoteDescription(description);
- this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
-
- // if we're running on FF, transform to Plan A first.
- if (RTCBrowserType.usesUnifiedPlan()) {
- description = this.interop.toUnifiedPlan(description);
- this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
- }
-
- if (RTCBrowserType.usesPlanB()) {
- description = normalizePlanB(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', dumpSDP(offer));
- // NOTE this is not tested because in meet the focus generates the
- // offer.
-
- // if we're running on FF, transform to Plan B first.
- if (RTCBrowserType.usesUnifiedPlan()) {
- offer = self.interop.toPlanB(offer);
- self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
- }
-
- offer = SSRCReplacement.mungeLocalVideoSSRC(offer);
-
- if (config.enableSimulcast && self.simulcast.isSupported()) {
- offer = self.simulcast.mungeLocalDescription(offer);
- self.trace('createOfferOnSuccess::postTransform (simulcast)', 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::preTransform', dumpSDP(answer));
- // if we're running on FF, transform to Plan A first.
- if (RTCBrowserType.usesUnifiedPlan()) {
- answer = self.interop.toPlanB(answer);
- self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer));
- }
-
- // munge local video SSRC
- answer = SSRCReplacement.mungeLocalVideoSSRC(answer);
-
- if (config.enableSimulcast && self.simulcast.isSupported()) {
- answer = self.simulcast.mungeLocalDescription(answer);
- self.trace('createAnswerOnSuccess::postTransform (simulcast)', 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) {
- // TODO: Is this the correct way to handle Opera, Temasys?
- if (RTCBrowserType.isFirefox()) {
- // ignore for now...
- if(!errback)
- errback = function () {};
- this.peerconnection.getStats(null, callback, errback);
- } else {
- this.peerconnection.getStats(callback);
- }
-};
-
-module.exports = TraceablePeerConnection;
-
-
-},{"../../service/xmpp/XMPPEvents":166,"../RTC/RTC":9,"../RTC/RTCBrowserType.js":10,"./LocalSSRCReplacement":57,"sdp-interop":94,"sdp-simulcast":97,"sdp-transform":100}],62:[function(require,module,exports){
-/* global $, $iq, APP, config, 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);
- }
- },
-
- onMucMemberLeft: 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.startAudioMuted !== undefined)
- {
- elem.c(
- 'property',
- { name: 'startAudioMuted', value: config.startAudioMuted})
- .up();
- }
- if(config.startVideoMuted !== undefined)
- {
- elem.c(
- 'property',
- { name: 'startVideoMuted', value: config.startVideoMuted})
- .up();
- }
- elem.c(
- 'property',
- { name: 'simulcastMode', value: 'rewriting'})
- .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) {
- eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED,
- focusComponent, 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":161,"../../service/xmpp/XMPPEvents":166,"../settings/Settings":49}],63:[function(require,module,exports){
-/* global $, $iq, config, connection, focusMucJid, messageHandler,
- 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;
-
-/**
- * The callback to update the recording button. Currently used from colibri
- * after receiving a pending status.
- */
-var recordingStateChangeCallback = null;
-
-function setRecordingToken(token) {
- recordingToken = token;
-}
-
-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 === 'on') ? 'start' : 'stop',
- mucjid: connection.emuc.roomjid});
- if (state === 'off'){
- 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 === 'on') ? 'started' : 'stopped') +
- '(jirecon)' + result);
- recordingEnabled = state;
- if (state === 'off'){
- 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 = recordingElem.attr('state');
-
- recordingEnabled = newState;
- callback(newState);
-
- if (newState === 'pending' && recordingStateChangeCallback == null) {
- recordingStateChangeCallback = callback;
- connection.addHandler(function(iq){
- var state = $(iq).find('recording').attr('state');
- if (state)
- recordingStateChangeCallback(state);
- }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
- }
- },
- function (error) {
- console.warn(error);
- callback(recordingEnabled);
- }
- );
-}
-
-function setRecording(state, token, callback, connection) {
- if (useJirecon){
- setRecordingJirecon(state, token, callback, connection);
- } else {
- setRecordingColibri(state, token, callback, connection);
- }
-}
-
-var Recording = {
- toggleRecording: function (tokenEmptyCallback, recordingStateChangeCallback, 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, recordingStateChangeCallback, connection);
- });
-
- return;
- }
-
- var oldState = recordingEnabled;
- var newState = (oldState === 'off' || !oldState) ? 'on' : 'off';
-
- setRecording(newState,
- 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);
- }
- recordingStateChangeCallback(state);
-
- },
- connection
- );
- }
-
-};
-
-module.exports = Recording;
-},{"./moderator":62}],64:[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");
-
-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,
- bridgeIsDown: false,
- init: function (conn) {
- this.connection = conn;
- },
- initPresenceMap: function (myroomjid) {
- this.presMap['to'] = myroomjid;
- this.presMap['xns'] = 'http://jabber.org/protocol/muc';
- if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted()) {
- this.addAudioInfoToPresence(true);
- }
- if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted()) {
- this.addVideoInfoToPresence(true);
- }
- },
- 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) {
- eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
- }
- }
-
- var url;
- // Parse prezi tag.
- var presentation = $(pres).find('>prezi');
- if (presentation.length) {
- 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) {
- 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) {
- eventEmitter.emit(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
- from, (audioMuted.text() === "true"));
- }
-
- // Parse video info tag.
- var videoMuted = $(pres).find('>videomuted');
- if (videoMuted.length) {
- eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_MUTED,
- from, (videoMuted.text() === "true"));
- }
-
- var startMuted = $(pres).find('>startmuted');
- if (startMuted.length && Moderator.isPeerModerator(from)) {
- eventEmitter.emit(XMPPEvents.START_MUTED_SETTING_CHANGED,
- startMuted.attr("audio") === "true",
- startMuted.attr("video") === "true");
- }
-
- var devices = $(pres).find('>devices');
- if(devices.length)
- {
- var audio = devices.find('>audio');
- var video = devices.find('>video');
- var devicesValues = {audio: false, video: false};
- if(audio.length && audio.text() === "true")
- {
- devicesValues.audio = true;
- }
-
- if(video.length && video.text() === "true")
- {
- devicesValues.video = true;
- }
- eventEmitter.emit(XMPPEvents.DEVICE_AVAILABLE,
- Strophe.getResourceFromJid(from), devicesValues);
- }
-
- var videoType = $(pres).find('>videoType');
- if (videoType.length)
- {
- if (videoType.text().length)
- {
- eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
- Strophe.getResourceFromJid(from), videoType.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.text() : null);
-
- if (from == this.myroomjid) {
- if (member.affiliation == 'owner') this.isOwner = true;
- if (this.role !== member.role) {
- this.role = member.role;
-
- eventEmitter.emit(XMPPEvents.LOCAL_ROLE_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_MEMBER_JOINED, 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();
- eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
-
- } else {
- console.warn('onPresError ', pres);
- eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
- }
- } else {
- console.warn('onPresError ', pres);
- eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, 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);
- }
- }
-
- // xep-0203 delay
- var stamp = $(msg).find('>delay').attr('stamp');
-
- if (!stamp) {
- // or xep-0091 delay, UTC timestamp
- stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
-
- if (stamp) {
- // the format is CCYYMMDDThh:mm:ss
- var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
- stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
- }
- }
-
- if (txt) {
- console.log('chat', nick, txt);
- eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
- from, nick, txt, this.myroomjid, stamp);
- }
- 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 () {
- if (!this.presMap['to']) {
- // Too early to send presence - not initialized
- return;
- }
- 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["devices"])
- {
- pres.c('devices').c('audio').t(this.presMap['devices'].audio).up()
- .c('video').t(this.presMap['devices'].video).up().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['videoTypeNs']) {
- pres.c('videoType', { xmlns: this.presMap['videoTypeNs'] })
- .t(this.presMap['videoType']).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["startMuted"] !== undefined)
- {
- pres.c("startmuted", {audio: this.presMap["startMuted"].audio,
- video: this.presMap["startMuted"].video,
- xmlns: "http://jitsi.org/jitmeet/start-muted"});
- delete this.presMap["startMuted"];
- }
-
- pres.up();
- this.connection.send(pres);
- },
- addDisplayNameToPresence: function (displayName) {
- this.presMap['displayName'] = displayName;
- },
- addDevicesToPresence: function (devices) {
- this.presMap['devices'] = devices;
- },
- /**
- * Adds the info about the type of our video stream.
- * @param videoType 'camera' or 'screen'
- */
- addVideoTypeToPresence: function (videoType) {
- this.presMap['videoTypeNs'] = 'http://jitsi.org/jitmeet/video';
- this.presMap['videoType'] = videoType;
- },
- 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];
- },
- 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;
- },
- addStartMutedToPresence: function (audio, video) {
- this.presMap["startMuted"] = {audio: audio, video: video};
- },
- 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_MEMBER_LEFT, jid);
-
- this.connection.jingle.terminateByJid(jid);
-
- if (this.getPrezi(jid)) {
- $(document).trigger('presentationremoved.muc',
- [jid, this.getPrezi(jid)]);
- }
-
- Moderator.onMucMemberLeft(jid);
- },
- parsePresence: function (from, member, pres) {
- if($(pres).find(">bridgeIsDown").length > 0 && !this.bridgeIsDown) {
- this.bridgeIsDown = true;
- eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
- }
-
- if(member.isFocus)
- return;
-
- var displayName = !config.displayJids
- ? member.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":166,"./moderator":62}],65:[function(require,module,exports){
-/* jshint -W117 */
-
-var JingleSession = require("./JingleSessionPC");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var RTCBrowserType = require("../RTC/RTCBrowserType");
-
-
-module.exports = function(XMPP, eventEmitter) {
- 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:apps:dtls:0');
- 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');
-
- if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()
- || RTCBrowserType.isTemasysPluginUsed()) {
- this.connection.disco.addFeature('urn:ietf:rfc:4588');
- }
-
- // this is dealt with by SDP O/A so we don't need to announce 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
-
- this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
- 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;
- }
- // local jid is not checked
- if (fromJid != 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':
- var startMuted = $(iq).find('jingle>startmuted');
- if (startMuted && startMuted.length > 0) {
- var audioMuted = startMuted.attr("audio");
- var videoMuted = startMuted.attr("video");
- eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
- audioMuted === "true", videoMuted === "true");
- }
- sess = new JingleSession(
- $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
- this.connection, XMPP, eventEmitter);
- // configure session
-
- sess.media_constraints = this.media_constraints;
- sess.pc_constraints = this.pc_constraints;
- sess.ice_config = this.ice_config;
-
- sess.initialize(fromJid, false);
- // FIXME: setRemoteDescription should only be done when this call is to be accepted
- sess.setOffer($(iq).find('>jingle'));
-
- this.sessions[sess.sid] = sess;
- this.jid2session[sess.peerjid] = sess;
-
- // the callback should either
- // .sendAnswer and .accept
- // or .sendTerminate -- not necessarily synchronous
-
- // TODO: do we check activecall == null?
- this.connection.jingle.activecall = sess;
-
- eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
-
- // TODO: check affiliation and/or role
- console.log('emuc data for', sess.peerjid,
- this.connection.emuc.members[sess.peerjid]);
- sess.sendAnswer();
- sess.accept();
- break;
- case 'session-accept':
- sess.setAnswer($(iq).find('>jingle'));
- 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, eventEmitter);
- // configure session
-
- sess.media_constraints = this.media_constraints;
- sess.pc_constraints = this.pc_constraints;
- sess.ice_config = this.ice_config;
-
- sess.initialize(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?
- },
-
- /**
- * Returns the data saved in 'updateLog' in a format to be logged.
- */
- getLog: function () {
- var data = {};
- var self = this;
- Object.keys(this.sessions).forEach(function (sid) {
- var session = self.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":166,"../RTC/RTCBrowserType":10,"./JingleSessionPC":56}],66:[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]);
- }
- });
-};
-},{}],67:[function(require,module,exports){
-/* global $, $iq, config, connection, focusMucJid, forceMuted,
- setAudioMuted, Strophe */
-/**
- * Moderate connection plugin.
- */
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-
-module.exports = function (XMPP, eventEmitter) {
- 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";
- eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, 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);
- }
- });
+function initCallback (err, msg) {
+ console.log("Initializing Status: err="+err+" msg="+msg);
}
-},{"../../service/xmpp/XMPPEvents":166}],68:[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;
- }
- );
- }
- }
- );
-};
-},{}],69:[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";
- }
- };
-};
+var CallStats = {
+ init: function (jingleSession) {
-},{}],70:[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 Settings = require("../settings/Settings");
-var Pako = require("pako");
-var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
-var RTCEvents = require("../../service/RTC/RTCEvents");
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
-var retry = require('retry');
-
-var eventEmitter = new EventEmitter();
-var connection = null;
-var authenticatedUser = false;
-
-function connect(jid, password) {
-
- var faultTolerantConnect = retry.operation({
- retries: 3
- });
-
- // fault tolerant connect
- faultTolerantConnect.attempt(function () {
-
- connection = XMPP.createConnection();
- Moderator.setConnection(connection);
-
- 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});
- }
-
- // Include user info in MUC presence
- var settings = Settings.getSettings();
- if (settings.email) {
- connection.emuc.addEmailToPresence(settings.email);
- }
- if (settings.uid) {
- connection.emuc.addUserIdToPresence(settings.uid);
- }
- if (settings.displayName) {
- connection.emuc.addDisplayNameToPresence(settings.displayName);
- }
-
-
- // connection.connect() starts the connection process.
- //
- // As the connection process proceeds, the user supplied callback will
- // be triggered multiple times with status updates. The callback should
- // take two arguments - the status code and the error condition.
- //
- // The status code will be one of the values in the Strophe.Status
- // constants. The error condition will be one of the conditions defined
- // in RFC 3920 or the condition ‘strophe-parsererror’.
- //
- // The Parameters wait, hold and route are optional and only relevant
- // for BOSH connections. Please see XEP 124 for a more detailed
- // explanation of the optional parameters.
- //
- // Connection status constants for use by the connection handler
- // callback.
- //
- // Status.ERROR - An error has occurred (websockets specific)
- // Status.CONNECTING - The connection is currently being made
- // Status.CONNFAIL - The connection attempt failed
- // Status.AUTHENTICATING - The connection is authenticating
- // Status.AUTHFAIL - The authentication attempt failed
- // Status.CONNECTED - The connection has succeeded
- // Status.DISCONNECTED - The connection has been terminated
- // Status.DISCONNECTING - The connection is currently being terminated
- // Status.ATTACHED - The connection has been attached
-
- var anonymousConnectionFailed = false;
- var connectionFailed = false;
- var lastErrorMsg;
- connection.connect(jid, password, function (status, msg) {
- console.log('Strophe status changed to',
- Strophe.getStatusString(status), msg);
- 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 {
- connectionFailed = true;
- }
- lastErrorMsg = msg;
- } else if (status === Strophe.Status.DISCONNECTED) {
- if (anonymousConnectionFailed) {
- // prompt user for username and password
- XMPP.promptLogin();
- } else {
-
- // Strophe already has built-in HTTP/BOSH error handling and
- // request retry logic. Requests are resent automatically
- // until their error count reaches 5. Strophe.js disconnects
- // if the error count is > 5. We are not replicating this
- // here.
- //
- // The "problem" is that failed HTTP/BOSH requests don't
- // trigger a callback with a status update, so when a
- // callback with status Strophe.Status.DISCONNECTED arrives,
- // we can't be sure if it's a graceful disconnect or if it's
- // triggered by some HTTP/BOSH error.
- //
- // But that's a minor issue in Jitsi Meet as we never
- // disconnect anyway, not even when the user closes the
- // browser window (which is kind of wrong, but the point is
- // that we should never ever get disconnected).
- //
- // On the other hand, failed connections due to XMPP layer
- // errors, trigger a callback with status Strophe.Status.CONNFAIL.
- //
- // Here we implement retry logic for failed connections due
- // to XMPP layer errors and we display an error to the user
- // if we get disconnected from the XMPP server permanently.
-
- // If the connection failed, retry.
- if (connectionFailed &&
- faultTolerantConnect.retry("connection-failed")) {
- return;
- }
-
- // If we failed to connect to the XMPP server, fire an event
- // to let all the interested module now about it.
- eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
- msg ? msg : lastErrorMsg);
- }
- } 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() {
- eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
-}
-
-function initStrophePlugins()
-{
- require("./strophe.emuc")(XMPP, eventEmitter);
- require("./strophe.jingle")(XMPP, eventEmitter);
- require("./strophe.moderate")(XMPP, eventEmitter);
- require("./strophe.util")();
- require("./strophe.rayo")();
- require("./strophe.logger")();
-}
-
-/**
- * If given localStream is video one this method will advertise it's
- * video type in MUC presence.
- * @param localStream new or modified LocalStream .
- */
-function broadcastLocalVideoType(localStream) {
- if (localStream.videoType)
- XMPP.addToPresence('videoType', localStream.videoType);
-}
-
-function registerListeners() {
- APP.RTC.addStreamListener(
- function (localStream) {
- maybeDoJoin();
- broadcastLocalVideoType(localStream);
- },
- StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
- );
- APP.RTC.addStreamListener(
- broadcastLocalVideoType,
- StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
- );
- APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
- XMPP.addToPresence("devices", devices);
- });
-}
-
-var unload = (function () {
- var unloaded = false;
-
- return function () {
- if (unloaded) { return; }
- unloaded = true;
-
- 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);
- };
-})();
-
-function setupEvents() {
- // In recent versions of FF the 'beforeunload' event is not fired when the
- // window or the tab is closed. It is only fired when we leave the page
- // (change URL). If this participant doesn't unload properly, then it
- // becomes a ghost for the rest of the participants that stay in the
- // conference. Thankfully handling the 'unload' event in addition to the
- // 'beforeunload' event seems to guarantee the execution of the 'unload'
- // method at least once.
- //
- // The 'unload' method can safely be run multiple times, it will actually do
- // something only the first time that it's run, so we're don't have to worry
- // about browsers that fire both events.
-
- $(window).bind('beforeunload', unload);
- $(window).bind('unload', unload);
-}
-
-var XMPP = {
- getConnection: function(){ return connection; },
- 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 () {
- eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN);
- },
- joinRoom: function(roomName, useNicks, nick) {
- var 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) {
- 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();
- }
- eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
- 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();
- },
- isConferenceInProgress: function () {
- return connection && connection.jingle.activecall &&
- connection.jingle.activecall.peerconnection;
- },
- switchStreams: function (stream, oldStream, callback, isAudio) {
- if (this.isConferenceInProgress()) {
- // FIXME: will block switchInProgress on true value in case of exception
- connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
- } else {
- // We are done immediately
- console.warn("No conference handler or conference not started yet");
- callback();
- }
- },
- sendVideoInfoPresence: function (mute) {
- if(!connection)
- return;
- connection.emuc.addVideoInfoToPresence(mute);
- connection.emuc.sendPresence();
- },
- setVideoMute: function (mute, callback, options) {
- if(!connection)
- return;
- var self = this;
- var localCallback = function (mute) {
- self.sendVideoInfoPresence(mute);
- return callback(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;
- }
-
- APP.RTC.localAudio.setMute(mute);
- this.sendAudioInfoPresence(mute, callback);
- return true;
- },
- sendAudioInfoPresence: function(mute, callback) {
- if(connection) {
- connection.emuc.addAudioInfoToPresence(mute);
- connection.emuc.sendPresence();
- }
- callback();
- return true;
- },
- toggleRecording: function (tokenEmptyCallback,
- recordingStateChangeCallback) {
- Recording.toggleRecording(tokenEmptyCallback,
- recordingStateChangeCallback, connection);
- },
- addToPresence: function (name, value, dontSend) {
- switch (name) {
- case "displayName":
- connection.emuc.addDisplayNameToPresence(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);
- break;
- case "devices":
- connection.emuc.addDevicesToPresence(value);
- break;
- case "videoType":
- connection.emuc.addVideoTypeToPresence(value);
- break;
- case "startMuted":
- if(!Moderator.isModerator())
- return;
- connection.emuc.addStartMutedToPresence(value[0],
- value[1]);
- break;
- default :
- console.log("Unknown tag for presence: " + name);
- 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;
- },
- // Gets the logs from strophe.jingle.
- getJingleLog: function () {
- return connection.jingle ? connection.jingle.getLog() : {};
- },
- // Gets the logs from strophe.
- getXmppLog: function () {
- return connection.logger ? connection.logger.log : 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 (!this.isConferenceInProgress())
- return null;
- return connection.jingle.activecall.getSsrcOwner(ssrc);
- },
- // Returns true iff we have joined the MUC.
- isMUCJoined: function () {
- return connection.emuc.joined;
- },
- getSessions: function () {
- return connection.jingle.sessions;
- },
- removeStream: function (stream) {
- if (!this.isConferenceInProgress())
- return;
- connection.jingle.activecall.peerconnection.removeStream(stream);
- }
-};
-
-module.exports = XMPP;
+ if(!config.callStatsID || !config.callStatsSecret || callStats !== null)
+ return;
-},{"../../service/RTC/RTCEvents":157,"../../service/RTC/StreamEventTypes":159,"../../service/xmpp/XMPPEvents":166,"../settings/Settings":49,"./SDP":58,"./moderator":62,"./recording":63,"./strophe.emuc":64,"./strophe.jingle":65,"./strophe.logger":66,"./strophe.moderate":67,"./strophe.rayo":68,"./strophe.util":69,"events":1,"pako":74,"retry":90}],71:[function(require,module,exports){
+ callStats = new callstats($, io, jsSHA);
+
+ this.session = jingleSession;
+ this.peerconnection = jingleSession.peerconnection.peerconnection;
+
+ this.userID = APP.xmpp.myResource();
+
+ var location = window.location;
+ this.confID = location.protocol + "//" +
+ location.hostname + location.pathname;
+
+ //userID is generated or given by the origin server
+ callStats.initialize(config.callStatsID,
+ config.callStatsSecret,
+ this.userID,
+ initCallback);
+
+ var usage = callStats.fabricUsage.multiplex;
+
+ callStats.addNewFabric(this.peerconnection,
+ Strophe.getResourceFromJid(jingleSession.peerjid),
+ usage,
+ this.confID,
+ this.pcCallback.bind(this));
+ },
+ pcCallback: function (err, msg) {
+ if (!callStats)
+ return;
+ console.log("Monitoring status: "+ err + " msg: " + msg);
+ callStats.sendFabricEvent(this.peerconnection,
+ callStats.fabricEvent.fabricSetup, this.confID);
+ },
+ sendMuteEvent: function (mute, type) {
+ if (!callStats)
+ return;
+ var event = null;
+ if (type === "video") {
+ event = (mute? callStats.fabricEvent.videoPause :
+ callStats.fabricEvent.videoResume);
+ }
+ else {
+ event = (mute? callStats.fabricEvent.audioMute :
+ callStats.fabricEvent.audioUnmute);
+ }
+ callStats.sendFabricEvent(this.peerconnection, event, this.confID);
+ },
+ sendTerminateEvent: function () {
+ if(!callStats) {
+ return;
+ }
+ callStats.sendFabricEvent(this.peerconnection,
+ callStats.fabricEvent.fabricTerminated, this.confID);
+ },
+ sendSetupFailedEvent: function () {
+ if(!callStats) {
+ return;
+ }
+ callStats.sendFabricEvent(this.peerconnection,
+ callStats.fabricEvent.fabricSetupFailed, this.confID);
+ }
+
+};
+module.exports = CallStats;
+},{"jssha":71,"socket.io-client":109}],49:[function(require,module,exports){
+/* global config */
+/**
+ * Provides statistics for the local stream.
+ */
+
+var RTCBrowserType = require('../RTC/RTCBrowserType');
+
+/**
+ * Size of the webaudio analyzer buffer.
+ * @type {number}
+ */
+var WEBAUDIO_ANALYZER_FFT_SIZE = 2048;
+
+/**
+ * Value of the webaudio analyzer smoothing time parameter.
+ * @type {number}
+ */
+var WEBAUDIO_ANALYZER_SMOOTING_TIME = 0.8;
+
+/**
+ * Converts time domain data array to audio level.
+ * @param samples 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.
+ * @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 ||
+ RTCBrowserType.isTemasysPluginUsed())
+ return;
+
+ var context = new AudioContext();
+ var analyser = context.createAnalyser();
+ analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
+ analyser.fftSize = WEBAUDIO_ANALYZER_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;
+},{"../RTC/RTCBrowserType":8}],50:[function(require,module,exports){
+/* global require, ssrc2jid */
+/* jshint -W117 */
+var RTCBrowserType = require("../RTC/RTCBrowserType");
+
+/* Whether we support the browser we are running into for logging statistics */
+var browserSupported = RTCBrowserType.isChrome() ||
+ RTCBrowserType.isOpera() || RTCBrowserType.isFirefox();
+/**
+ * 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) {
+ var browserType = RTCBrowserType.getBrowserType();
+ if (!keyMap[browserType][name])
+ throw "The property isn't supported!";
+ var key = keyMap[browserType][name];
+ return (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) ?
+ 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 && browserSupported) {
+ 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 && (browserSupported && !RTCBrowserType.isFirefox())) {
+ 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"
+};
+keyMap[RTCBrowserType.RTC_BROWSER_OPERA] =
+ keyMap[RTCBrowserType.RTC_BROWSER_CHROME];
+
+
+/**
+ * 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) {
+ if((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);
+ }
+ }
+};
+
+},{"../RTC/RTCBrowserType":8}],51:[function(require,module,exports){
+/* global require, APP */
+/**
+ * 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 CallStats = require("./CallStats");
+var RTCEvents = require("../../service/RTC/RTCEvents");
+
+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 = 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) {
+ CallStats.sendTerminateEvent();
+ 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);
+ //FIXME: we may want to change CALL INCOMING event to onnegotiationneeded
+ APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
+ startRemoteStats(event.peerconnection);
+// CallStats.init(event);
+ });
+ APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY, function (session) {
+ CallStats.init(session);
+ });
+ APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) {
+ CallStats.sendMuteEvent(mute, "audio");
+ });
+ APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
+ CallStats.sendSetupFailedEvent();
+ });
+ APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) {
+ CallStats.sendMuteEvent(mute, "video");
+ });
+ }
+};
+
+
+
+
+module.exports = statistics;
+},{"../../service/RTC/RTCEvents":160,"../../service/RTC/StreamEventTypes.js":162,"../../service/xmpp/XMPPEvents":169,"./CallStats":48,"./LocalStatsCollector.js":49,"./RTPStatsCollector.js":50,"events":170}],52:[function(require,module,exports){
+/* global $, require, config, interfaceConfig */
+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,
+ 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":168,"../settings/Settings":47,"i18next-client":70}],53:[function(require,module,exports){
+/*
+ * JingleSession provides an API to manage a single Jingle session. We will
+ * have different implementations depending on the underlying interface used
+ * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
+ */
+function JingleSession(me, sid, connection, service, eventEmitter) {
+ /**
+ * Our JID.
+ */
+ this.me = me;
+
+ /**
+ * The Jingle session identifier.
+ */
+ this.sid = sid;
+
+ /**
+ * The XMPP connection.
+ */
+ this.connection = connection;
+
+ /**
+ * The XMPP service.
+ */
+ this.service = service;
+
+ /**
+ * The event emitter.
+ */
+ this.eventEmitter = eventEmitter;
+
+ /**
+ * Whether to use dripping or not. Dripping is sending trickle candidates
+ * not one-by-one.
+ * Note: currently we do not support 'false'.
+ */
+ this.usedrip = true;
+
+ /**
+ * When dripping is used, stores ICE candidates which are to be sent.
+ */
+ this.drip_container = [];
+
+ // Media constraints. Is this WebRTC only?
+ this.media_constraints = null;
+
+ // ICE servers config (RTCConfiguration?).
+ this.ice_config = {};
+}
+
+/**
+ * Prepares this object to initiate a session.
+ * @param peerjid the JID of the remote peer.
+ * @param isInitiator whether we will be the Jingle initiator.
+ * @param media_constraints
+ * @param ice_config
+ */
+JingleSession.prototype.initialize = function(peerjid, isInitiator,
+ media_constraints, ice_config) {
+ this.media_constraints = media_constraints;
+ this.ice_config = ice_config;
+
+ if (this.state !== null) {
+ console.error('attempt to initiate on session ' + this.sid +
+ 'in state ' + this.state);
+ return;
+ }
+ this.state = 'pending';
+ this.initiator = isInitiator ? this.me : peerjid;
+ this.responder = !isInitiator ? this.me : peerjid;
+ this.peerjid = peerjid;
+
+ this.doInitialize();
+};
+
+/**
+ * Finishes initialization.
+ */
+JingleSession.prototype.doInitialize = function() {};
+
+/**
+ * Adds the ICE candidates found in the 'contents' array as remote candidates?
+ * Note: currently only used on transport-info
+ */
+JingleSession.prototype.addIceCandidates = function(contents) {};
+
+/**
+ * Handles an 'add-source' event.
+ *
+ * @param contents an array of Jingle 'content' elements.
+ */
+JingleSession.prototype.addSources = function(contents) {};
+
+/**
+ * Handles a 'remove-source' event.
+ *
+ * @param contents an array of Jingle 'content' elements.
+ */
+JingleSession.prototype.removeSources = function(contents) {};
+
+/**
+ * Terminates this Jingle session (stops sending media and closes the streams?)
+ */
+JingleSession.prototype.terminate = function() {};
+
+/**
+ * Sends a Jingle session-terminate message to the peer and terminates the
+ * session.
+ * @param reason
+ * @param text
+ */
+JingleSession.prototype.sendTerminate = function(reason, text) {};
+
+/**
+ * Handles an offer from the remote peer (prepares to accept a session).
+ * @param jingle the 'jingle' XML element.
+ */
+JingleSession.prototype.setOffer = function(jingle) {};
+
+/**
+ * Handles an answer from the remote peer (prepares to accept a session).
+ * @param jingle the 'jingle' XML element.
+ */
+JingleSession.prototype.setAnswer = function(jingle) {};
+
+
+module.exports = JingleSession;
+
+},{}],54:[function(require,module,exports){
+/* jshint -W117 */
+var JingleSession = require("./JingleSession");
+var TraceablePeerConnection = require("./TraceablePeerConnection");
+var SDPDiffer = require("./SDPDiffer");
+var SDPUtil = require("./SDPUtil");
+var SDP = require("./SDP");
+var async = require("async");
+var transform = require("sdp-transform");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var RTCBrowserType = require("../RTC/RTCBrowserType");
+var SSRCReplacement = require("./LocalSSRCReplacement");
+
+// Jingle stuff
+function JingleSessionPC(me, sid, connection, service, eventEmitter) {
+ JingleSession.call(this, me, sid, connection, service, eventEmitter);
+ this.initiator = null;
+ this.responder = null;
+ this.peerjid = null;
+ this.state = null;
+ this.localSDP = null;
+ this.remoteSDP = null;
+ this.relayedStreams = [];
+ this.pc_constraints = null;
+
+ this.usetrickle = true;
+ this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
+
+ 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;
+ this.ssrcOwners = {};
+ this.ssrcVideoTypes = {};
+ this.eventEmitter = eventEmitter;
+
+ /**
+ * 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;
+ this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1);
+ // We start with the queue paused. We resume it when the signaling state is
+ // stable and the ice connection state is connected.
+ this.modifySourcesQueue.pause();
+}
+JingleSessionPC.prototype = JingleSession.prototype;
+JingleSessionPC.prototype.constructor = JingleSessionPC;
+
+
+JingleSessionPC.prototype.setOffer = function(offer) {
+ this.setRemoteDescription(offer, 'offer');
+};
+
+JingleSessionPC.prototype.setAnswer = function(answer) {
+ this.setRemoteDescription(answer, 'answer');
+};
+
+JingleSessionPC.prototype.updateModifySourcesQueue = function() {
+ var signalingState = this.peerconnection.signalingState;
+ var iceConnectionState = this.peerconnection.iceConnectionState;
+ if (signalingState === 'stable' && iceConnectionState === 'connected') {
+ this.modifySourcesQueue.resume();
+ } else {
+ this.modifySourcesQueue.pause();
+ }
+};
+
+JingleSessionPC.prototype.doInitialize = function () {
+ var self = this;
+
+ this.hadstuncandidate = false;
+ this.hadturncandidate = false;
+ this.lasticecandidate = false;
+ this.isreconnect = false;
+
+ this.peerconnection = new TraceablePeerConnection(
+ this.connection.jingle.ice_config,
+ this.connection.jingle.pc_constraints,
+ this);
+
+ this.peerconnection.onicecandidate = function (event) {
+ self.sendIceCandidate(event.candidate);
+ };
+ this.peerconnection.onaddstream = function (event) {
+ if (event.stream.id !== 'default') {
+ console.log("REMOTE STREAM ADDED: ", event.stream , event.stream.id);
+ self.remoteStreamAdded(event);
+ } else {
+ // This is a recvonly stream. Clients that implement Unified Plan,
+ // such as Firefox use recvonly "streams/channels/tracks" for
+ // receiving remote stream/tracks, as opposed to Plan B where there
+ // are only 3 channels: audio, video and data.
+ console.log("RECVONLY REMOTE STREAM IGNORED: " + event.stream + " - " + event.stream.id);
+ }
+ };
+ 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;
+ self.updateModifySourcesQueue();
+ };
+ /**
+ * The oniceconnectionstatechange event handler contains the code to execute when the iceconnectionstatechange event,
+ * of type Event, is received by this RTCPeerConnection. Such an event is sent when the value of
+ * RTCPeerConnection.iceConnectionState changes.
+ *
+ * @param event the event containing information about the change
+ */
+ this.peerconnection.oniceconnectionstatechange = function (event) {
+ if (!(self && self.peerconnection)) return;
+ self.updateModifySourcesQueue();
+ switch (self.peerconnection.iceConnectionState) {
+ case 'connected':
+
+ // Informs interested parties that the connection has been restored.
+ if (self.peerconnection.signalingState === 'stable' && self.isreconnect)
+ self.eventEmitter.emit(XMPPEvents.CONNECTION_RESTORED);
+ self.isreconnect = false;
+
+ break;
+ case 'disconnected':
+ self.isreconnect = true;
+ // Informs interested parties that the connection has been interrupted.
+ if (self.peerconnection.signalingState === 'stable')
+ self.eventEmitter.emit(XMPPEvents.CONNECTION_INTERRUPTED);
+ break;
+ case 'failed':
+ self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ break;
+ }
+ onIceConnectionStateChange(self.sid, self);
+ };
+ this.peerconnection.onnegotiationneeded = function (event) {
+ self.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, 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;
+ }
+}
+
+JingleSessionPC.prototype.accept = function () {
+ 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');
+ }
+ 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 });
+ // FIXME why do we generate session-accept in 3 different places ?
+ 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();
+
+ SSRCReplacement.processSessionInit(accept);
+
+ 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';
+ JingleSessionPC.onJingleError(self.sid, error);
+ },
+ 10000);
+ },
+ function (e) {
+ console.error('setLocalDescription failed', e);
+ self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ }
+ );
+};
+
+JingleSessionPC.prototype.terminate = function (reason) {
+ this.state = 'ended';
+ this.reason = reason;
+ this.peerconnection.close();
+ if (this.statsinterval !== null) {
+ window.clearInterval(this.statsinterval);
+ this.statsinterval = null;
+ }
+};
+
+JingleSessionPC.prototype.active = function () {
+ return this.state == 'active';
+};
+
+JingleSessionPC.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...');
+ //FIXME why do we generate session-accept in 3 different places ?
+ 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 sendJingle = function (ssrc) {
+ if(!ssrc)
+ ssrc = {};
+ self.localSDP.toJingle(
+ init,
+ self.initiator == self.me ? 'initiator' : 'responder',
+ ssrc);
+
+ SSRCReplacement.processSessionInit(init);
+
+ 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';
+ JingleSessionPC.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]);
+ }
+ }
+};
+
+JingleSessionPC.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';
+ JingleSessionPC.onJingleError(this.sid, error);
+ },
+ 10000);
+};
+
+
+JingleSessionPC.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
+ );
+};
+
+// FIXME createdOffer is never used in jitsi-meet
+JingleSessionPC.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);
+
+ SSRCReplacement.processSessionInit(init);
+
+ 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';
+ JingleSessionPC.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);
+ self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ }
+ );
+ 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;
+ }
+ }
+};
+
+JingleSessionPC.prototype.readSsrcInfo = function (contents) {
+ var self = this;
+ $(contents).each(function (idx, content) {
+ var name = $(content).attr('name');
+ var mediaType = this.getAttribute('name');
+ var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
+ ssrcs.each(function () {
+ var ssrc = this.getAttribute('ssrc');
+ $(this).find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]').each(
+ function () {
+ var owner = this.getAttribute('owner');
+ self.ssrcOwners[ssrc] = owner;
+ }
+ );
+ });
+ });
+};
+
+JingleSessionPC.prototype.getSsrcOwner = function (ssrc) {
+ return this.ssrcOwners[ssrc];
+};
+
+JingleSessionPC.prototype.setRemoteDescription = function (elem, desctype) {
+ //console.log('setting remote description... ', desctype);
+ this.remoteSDP = new SDP('');
+ this.remoteSDP.fromJingle(elem);
+ this.readSsrcInfo($(elem).find(">content"));
+ 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);
+ JingleSessionPC.onJingleFatalError(self, e);
+ }
+ );
+};
+
+JingleSessionPC.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);
+ }
+ });
+ });
+};
+
+JingleSessionPC.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);
+ self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ },
+ this.media_constraints
+ );
+};
+
+JingleSessionPC.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) {
+ // FIXME why do we generate session-accept in 3 different places ?
+ 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 });
+ self.localSDP.toJingle(
+ accept,
+ self.initiator == self.me ? 'initiator' : 'responder',
+ ssrcs);
+
+ SSRCReplacement.processSessionInit(accept);
+
+ 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';
+ JingleSessionPC.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);
+ self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ }
+ );
+ 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;
+ }
+ }
+};
+
+JingleSessionPC.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;
+ }
+};
+
+JingleSessionPC.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);
+
+ this.readSsrcInfo(elem);
+
+ 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;
+ }
+ if (sdp.containsSSRC(ssrc)) {
+ console.warn("Source-add request for existing 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.modifySourcesQueue.push(function() {
+ // When a source is added and if this is FF, a new channel is allocated
+ // for receiving the added source. We need to diffuse the SSRC of this
+ // new recvonly channel to the rest of the peers.
+ console.log('modify sources done');
+
+ var newSdp = new SDP(self.peerconnection.localDescription.sdp);
+ console.log("SDPs", mySdp, newSdp);
+ self.notifyMySSRCUpdate(mySdp, newSdp);
+ });
+};
+
+JingleSessionPC.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.modifySourcesQueue.push(function() {
+ // When a source is removed and if this is FF, the recvonly channel that
+ // receives the remote stream is deactivated . We need to diffuse the
+ // recvonly SSRC removal to the rest of the peers.
+ console.log('modify sources done');
+
+ var newSdp = new SDP(self.peerconnection.localDescription.sdp);
+ console.log("SDPs", mySdp, newSdp);
+ self.notifyMySSRCUpdate(mySdp, newSdp);
+ });
+};
+
+JingleSessionPC.prototype._modifySources = function (successCallback, queueCallback) {
+ 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();
+ }
+ queueCallback();
+ 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 = [];
+
+ 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");
+ queueCallback("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();
+ }
+ queueCallback();
+ },
+ function(error) {
+ console.error('modified setLocalDescription failed', error);
+ queueCallback(error);
+ }
+ );
+ },
+ function(error) {
+ console.error('modified answer failed', error);
+ queueCallback(error);
+ }
+ );
+ },
+ function(error) {
+ console.error('modify failed', error);
+ queueCallback(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.
+ */
+JingleSessionPC.prototype.switchStreams = function (new_stream, oldStream, success_callback, isAudio) {
+
+ 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);
+ if(new_stream)
+ self.peerconnection.addStream(new_stream);
+ }
+
+ // Conference is not active
+ if(!oldSdp || !self.peerconnection) {
+ success_callback();
+ return;
+ }
+
+ self.switchstreams = true;
+ self.modifySourcesQueue.push(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.
+ */
+JingleSessionPC.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);
+
+ // Let 'source-remove' IQ through the hack and see if we're allowed to send
+ // it in the current form
+ if (removed)
+ remove = SSRCReplacement.processSourceRemove(remove);
+
+ if (removed && remove) {
+ console.info("Sending source-remove", remove);
+ 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);
+
+ // Let 'source-add' IQ through the hack and see if we're allowed to send
+ // it in the current form
+ if (added)
+ add = SSRCReplacement.processSourceAdd(add);
+
+ if (added && add) {
+ console.info("Sending source-add", add);
+ 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');
+ }
+};
+
+/**
+ * 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)
+ */
+JingleSessionPC.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);
+
+ var self = this;
+ var oldSdp = null;
+ if(self.peerconnection) {
+ if(self.peerconnection.localDescription) {
+ oldSdp = new SDP(self.peerconnection.localDescription.sdp);
+ }
+ }
+
+ this.modifySourcesQueue.push(function() {
+ console.log('modify sources done');
+
+ callback(mute);
+
+ var newSdp = new SDP(self.peerconnection.localDescription.sdp);
+ console.log("SDPs", oldSdp, newSdp);
+ self.notifyMySSRCUpdate(oldSdp, newSdp);
+ });
+};
+
+JingleSessionPC.prototype.hardMuteVideo = function (muted) {
+ this.pendingop = muted ? 'mute' : 'unmute';
+};
+
+JingleSessionPC.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);
+};
+
+JingleSessionPC.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);
+};
+
+JingleSessionPC.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;
+};
+
+JingleSessionPC.onJingleError = function (session, error)
+{
+ console.error("Jingle error", error);
+}
+
+JingleSessionPC.onJingleFatalError = function (session, error)
+{
+ this.service.sessionTerminated = true;
+ this.connection.emuc.doLeave();
+ this.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED);
+ this.eventEmitter.emit(XMPPEvents.JINGLE_FATAL_ERROR, session, error);
+}
+
+JingleSessionPC.prototype.setLocalDescription = function () {
+ var self = this;
+ var newssrcs = [];
+ var session = transform.parse(this.peerconnection.localDescription.sdp);
+ session.media.forEach(function (media) {
+
+ if (media.ssrcs != null && media.ssrcs.length > 0) {
+ // TODO(gp) maybe exclude FID streams?
+ media.ssrcs.forEach(function (ssrc) {
+ if (ssrc.attribute !== 'cname') {
+ return;
+ }
+ newssrcs.push({
+ 'ssrc': ssrc.id,
+ 'type': media.type
+ });
+ });
+ }
+ else if(self.localStreamsSSRC && self.localStreamsSSRC[media.type])
+ {
+ newssrcs.push({
+ 'ssrc': self.localStreamsSSRC[media.type],
+ 'type': media.type
+ });
+ }
+
+ });
+
+ console.log('new ssrcs', newssrcs);
+
+ // Bind us as local SSRCs owner
+ if (newssrcs.length > 0) {
+ for (var i = 1; i <= newssrcs.length; i ++) {
+ var ssrc = newssrcs[i-1].ssrc;
+ var myJid = self.connection.emuc.myroomjid;
+ self.ssrcOwners[ssrc] = myJid;
+ }
+ }
+}
+
+// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
+// TODO: is this hack (along with the XMPPEvent-s used only for it) still needed
+// now that we announce an SSRC for receive-only streams?
+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);
+ eventEmitter.emit(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR);
+ }
+ );
+ },
+ function (error) {
+ console.log('triggerKeyframe createAnswer failed', error);
+ eventEmitter.emit(XMPPEvents.CREATE_ANSWER_ERROR);
+ }
+ );
+ },
+ function (error) {
+ console.log('triggerKeyframe setRemoteDescription failed', error);
+ eventEmitter.emit(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR);
+ }
+ );
+}
+
+
+JingleSessionPC.prototype.remoteStreamAdded = function (data, times) {
+ var self = this;
+ var thessrc;
+ var streamId = APP.RTC.getStreamID(data.stream);
+
+ // look up an associated JID for a stream id
+ if (!streamId) {
+ console.error("No stream ID for", data.stream);
+ } else if (streamId && streamId.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;
+
+ if (RTCBrowserType.isTemasysPluginUsed()) {
+ return ((line.indexOf('mslabel:' + streamId) !== -1));
+ } else {
+ return ((line.indexOf('msid:' + streamId) !== -1));
+ }
+ });
+ if (ssrclines.length) {
+ thessrc = ssrclines[0].substring(7).split(' ')[0];
+
+ if (!self.ssrcOwners[thessrc]) {
+ console.error("No SSRC owner known for: " + thessrc);
+ return;
+ }
+ data.peerjid = self.ssrcOwners[thessrc];
+ console.log('associated jid', self.ssrcOwners[thessrc]);
+ } else {
+ console.error("No SSRC lines for ", streamId);
+ }
+ }
+
+ 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
+ // TODO: is this hack still needed now that we announce an SSRC for
+ // receive-only streams?
+ 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 = JingleSessionPC;
+
+},{"../../service/xmpp/XMPPEvents":169,"../RTC/RTCBrowserType":8,"./JingleSession":53,"./LocalSSRCReplacement":55,"./SDP":56,"./SDPDiffer":57,"./SDPUtil":58,"./TraceablePeerConnection":59,"async":69,"sdp-transform":106}],55:[function(require,module,exports){
+/* global $ */
+
+/*
+ Here we do modifications of local video SSRCs. There are 2 situations we have
+ to handle:
+
+ 1. We generate SSRC for local recvonly video stream. This is the case when we
+ have no local camera and it is not generated automatically, but SSRC=1 is
+ used implicitly. If that happens RTCP packets will be dropped by the JVB
+ and we won't be able to request video key frames correctly.
+
+ 2. A hack to re-use SSRC of the first video stream for any new stream created
+ in future. It turned out that Chrome may keep on using the SSRC of removed
+ video stream in RTCP even though a new one has been created. So we just
+ want to avoid that by re-using it. Jingle 'source-remove'/'source-add'
+ notifications are blocked once first video SSRC is advertised to the focus.
+
+ What this hack does:
+
+ 1. Stores the SSRC of the first video stream created by
+ a) scanning Jingle session-accept/session-invite for existing video SSRC
+ b) watching for 'source-add' for new video stream if it has not been
+ created in step a)
+ 2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with
+ the stored one. It is called by 'TracablePeerConnection' before local SDP is
+ returned to the other parts of the application.
+ 3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and
+ blocks those notifications. This makes Jicofo and all participants think
+ that it exists all the time even if the video stream has been removed or
+ replaced locally. Thanks to that there is no additional signaling activity
+ on video mute or when switching to the desktop stream.
+ */
+
+var SDP = require('./SDP');
+var RTCBrowserType = require('../RTC/RTCBrowserType');
+
+/**
+ * The hack is enabled on all browsers except FF by default
+ * FIXME finish the hack once removeStream method is implemented in FF
+ * @type {boolean}
+ */
+var isEnabled = !RTCBrowserType.isFirefox();
+
+/**
+ * Stored SSRC of local video stream.
+ */
+var localVideoSSRC;
+
+/**
+ * SSRC used for recvonly video stream when we have no local camera.
+ * This is in order to tell Chrome what SSRC should be used in RTCP requests
+ * instead of 1.
+ */
+var localRecvOnlySSRC;
+
+/**
+ * cname for localRecvOnlySSRC
+ */
+var localRecvOnlyCName;
+
+/**
+ * Method removes element which describes localVideoSSRC
+ * from given Jingle IQ.
+ * @param modifyIq 'source-add' or 'source-remove' Jingle IQ.
+ * @param actionName display name of the action which will be printed in log
+ * messages.
+ * @returns {*} modified Jingle IQ, so that it does not contain element
+ * corresponding to localVideoSSRC or null if no
+ * other SSRCs left to be signaled after removing it.
+ */
+var filterOutSource = function (modifyIq, actionName) {
+ var modifyIqTree = $(modifyIq.tree());
+
+ if (!localVideoSSRC)
+ return modifyIqTree[0];
+
+ var videoSSRC = modifyIqTree.find(
+ '>jingle>content[name="video"]' +
+ '>description>source[ssrc="' + localVideoSSRC + '"]');
+
+ if (!videoSSRC.length) {
+ return modifyIqTree[0];
+ }
+
+ console.info(
+ 'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC);
+
+ videoSSRC.remove();
+
+ // Check if any sources still left to be added/removed
+ if (modifyIqTree.find('>jingle>content>description>source').length) {
+ return modifyIqTree[0];
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Scans given Jingle IQ for video SSRC and stores it.
+ * @param jingleIq the Jingle IQ to be scanned for video SSRC.
+ */
+var storeLocalVideoSSRC = function (jingleIq) {
+ var videoSSRCs =
+ $(jingleIq.tree())
+ .find('>jingle>content[name="video"]>description>source');
+
+ videoSSRCs.each(function (idx, ssrcElem) {
+ if (localVideoSSRC)
+ return;
+ // We consider SSRC real only if it has msid attribute
+ // recvonly streams in FF do not have it as well as local SSRCs
+ // we generate for recvonly streams in Chrome
+ var ssrSel = $(ssrcElem);
+ var msid = ssrSel.find('>parameter[name="msid"]');
+ if (msid.length) {
+ var ssrcVal = ssrSel.attr('ssrc');
+ if (ssrcVal) {
+ localVideoSSRC = ssrcVal;
+ console.info('Stored local video SSRC' +
+ ' for future re-use: ' + localVideoSSRC);
+ }
+ }
+ });
+};
+
+/**
+ * Generates new SSRC for local video recvonly stream.
+ * FIXME what about eventual SSRC collision ?
+ */
+function generateRecvonlySSRC() {
+ //
+ localRecvOnlySSRC =
+ Math.random().toString(10).substring(2, 11);
+ localRecvOnlyCName =
+ Math.random().toString(36).substring(2);
+ console.info(
+ "Generated local recvonly SSRC: " + localRecvOnlySSRC +
+ ", cname: " + localRecvOnlyCName);
+}
+
+var LocalSSRCReplacement = {
+ /**
+ * Method must be called before 'session-initiate' or 'session-invite' is
+ * sent. Scans the IQ for local video SSRC and stores it if detected.
+ *
+ * @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ
+ * which will be scanned for local video SSRC.
+ */
+ processSessionInit: function (sessionInit) {
+ if (!isEnabled)
+ return;
+
+ if (localVideoSSRC) {
+ console.error("Local SSRC stored already: " + localVideoSSRC);
+ return;
+ }
+ storeLocalVideoSSRC(sessionInit);
+ },
+ /**
+ * If we have local video SSRC stored searched given
+ * localDescription for video SSRC and makes sure it is replaced
+ * with the stored one.
+ * @param localDescription local description object that will have local
+ * video SSRC replaced with the stored one
+ * @returns modified localDescription object.
+ */
+ mungeLocalVideoSSRC: function (localDescription) {
+ if (!isEnabled)
+ return localDescription;
+
+ if (!localDescription) {
+ console.warn("localDescription is null or undefined");
+ return localDescription;
+ }
+
+ // IF we have local video SSRC stored make sure it is replaced
+ // with old SSRC
+ if (localVideoSSRC) {
+ var newSdp = new SDP(localDescription.sdp);
+ if (newSdp.media[1].indexOf("a=ssrc:") !== -1 &&
+ !newSdp.containsSSRC(localVideoSSRC)) {
+ // Get new video SSRC
+ var map = newSdp.getMediaSsrcMap();
+ var videoPart = map[1];
+ var videoSSRCs = videoPart.ssrcs;
+ var newSSRC = Object.keys(videoSSRCs)[0];
+
+ console.info(
+ "Replacing new video SSRC: " + newSSRC +
+ " with " + localVideoSSRC);
+
+ localDescription.sdp =
+ newSdp.raw.replace(
+ new RegExp('a=ssrc:' + newSSRC, 'g'),
+ 'a=ssrc:' + localVideoSSRC);
+ }
+ } else {
+ // Make sure we have any SSRC for recvonly video stream
+ var sdp = new SDP(localDescription.sdp);
+
+ if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 &&
+ sdp.media[1].indexOf('a=recvonly') !== -1) {
+
+ if (!localRecvOnlySSRC) {
+ generateRecvonlySSRC();
+ }
+
+ console.info('No SSRC in video recvonly stream' +
+ ' - adding SSRC: ' + localRecvOnlySSRC);
+
+ sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC +
+ ' cname:' + localRecvOnlyCName + '\r\n';
+
+ localDescription.sdp = sdp.session + sdp.media.join('');
+ }
+ }
+ return localDescription;
+ },
+ /**
+ * Method must be called before 'source-add' notification is sent. In case
+ * we have local video SSRC advertised already it will be removed from the
+ * notification. If no other SSRCs are described by given IQ null will be
+ * returned which means that there is no point in sending the notification.
+ * @param sourceAdd 'source-add' Jingle IQ to be processed
+ * @returns modified 'source-add' IQ which can be sent to the focus or
+ * null if no notification shall be sent. It is no longer
+ * a Strophe IQ Builder instance, but DOM element tree.
+ */
+ processSourceAdd: function (sourceAdd) {
+ if (!isEnabled)
+ return sourceAdd;
+
+ if (!localVideoSSRC) {
+ // Store local SSRC if available
+ storeLocalVideoSSRC(sourceAdd);
+ return sourceAdd;
+ } else {
+ return filterOutSource(sourceAdd, 'source-add');
+ }
+ },
+ /**
+ * Method must be called before 'source-remove' notification is sent.
+ * Removes local video SSRC from the notification. If there are no other
+ * SSRCs described in the given IQ null will be returned which
+ * means that there is no point in sending the notification.
+ * @param sourceRemove 'source-remove' Jingle IQ to be processed
+ * @returns modified 'source-remove' IQ which can be sent to the focus or
+ * null if no notification shall be sent. It is no longer
+ * a Strophe IQ Builder instance, but DOM element tree.
+ */
+ processSourceRemove: function (sourceRemove) {
+ if (!isEnabled)
+ return sourceRemove;
+
+ return filterOutSource(sourceRemove, 'source-remove');
+ },
+
+ /**
+ * Turns the hack on or off
+ * @param enabled true to enable the hack or false
+ * to disable it
+ */
+ setEnabled: function (enabled) {
+ isEnabled = enabled;
+ }
+};
+
+module.exports = LocalSSRCReplacement;
+
+},{"../RTC/RTCBrowserType":8,"./SDP":56}],56:[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 {
+ var k = kv.split(':', 2)[0];
+ elem.attrs({ name: k });
+
+ var v = kv.split(':', 2)[1];
+ v = SDPUtil.filter_special_chars(v);
+ elem.attrs({ value: v });
+ }
+ 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 = SDPUtil.filter_special_chars(msid);
+ 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 () {
+ var name = this.getAttribute('name');
+ var value = this.getAttribute('value');
+ value = SDPUtil.filter_special_chars(value);
+ media += 'a=ssrc:' + ssrc + ' ' + name;
+ if (value && value.length)
+ media += ':' + value;
+ media += '\r\n';
+ });
+ });
+
+ return media;
+};
+
+
+module.exports = SDP;
+
+
+},{"./SDPUtil":58}],57:[function(require,module,exports){
+
+var SDPUtil = require("./SDPUtil");
+
+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 {
+ var nv = kv.split(':', 2);
+ var name = nv[0];
+ var value = SDPUtil.filter_special_chars(nv[1]);
+ modify.attrs({ name: name });
+ modify.attrs({ value: value });
+ }
+ 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;
+},{"./SDPUtil":58}],58:[function(require,module,exports){
+SDPUtil = {
+ filter_special_chars: function (text) {
+ return text.replace(/[\\\/\{,\}\+]/g, "");
+ },
+ 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;
+},{}],59:[function(require,module,exports){
+var RTC = require('../RTC/RTC');
+var RTCBrowserType = require("../RTC/RTCBrowserType.js");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var SSRCReplacement = require("./LocalSSRCReplacement");
+
+function TraceablePeerConnection(ice_config, constraints, session) {
+ var self = this;
+ var RTCPeerConnectionType = null;
+ if (RTCBrowserType.isFirefox()) {
+ RTCPeerConnectionType = mozRTCPeerConnection;
+ } else if (RTCBrowserType.isTemasysPluginUsed()) {
+ RTCPeerConnectionType = RTCPeerConnection;
+ } else {
+ RTCPeerConnectionType = webkitRTCPeerConnection;
+ }
+ this.peerconnection = new RTCPeerConnectionType(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();
+ var Simulcast = require('sdp-simulcast');
+ this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
+
+ // override as desired
+ this.trace = function (what, info) {
+ /*console.warn('WTRACE', what, info);
+ if (info && RTCBrowserType.isIExplorer()) {
+ if (info.length > 1024) {
+ console.warn('WTRACE', what, info.substr(1024));
+ }
+ if (info.length > 2048) {
+ console.warn('WTRACE', what, info.substr(2048));
+ }
+ }*/
+ self.updateLog.push({
+ time: new Date(),
+ type: what,
+ value: info || ""
+ });
+ };
+ this.onicecandidate = null;
+ this.peerconnection.onicecandidate = function (event) {
+ // FIXME: this causes stack overflow with Temasys Plugin
+ if (!RTCBrowserType.isTemasysPluginUsed())
+ 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);
+ }
+ };
+ // XXX: do all non-firefox browsers which we support also support this?
+ if (!RTCBrowserType.isFirefox() && this.maxstats) {
+ this.statsinterval = window.setInterval(function() {
+ self.peerconnection.getStats(function(stats) {
+ var results = stats.result();
+ var now = new Date();
+ for (var i = 0; i < results.length; ++i) {
+ 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);
+ }
+}
+
+/**
+ * Returns a string representation of a SessionDescription object.
+ */
+var dumpSDP = function(description) {
+ if (typeof description === 'undefined' || description == null) {
+ return '';
+ }
+
+ return 'type: ' + description.type + '\r\n' + description.sdp;
+};
+
+/**
+ * Takes a SessionDescription object and returns a "normalized" version.
+ * Currently it only takes care of ordering the a=ssrc lines.
+ */
+var normalizePlanB = function(desc) {
+ if (typeof desc !== 'object' || desc === null ||
+ typeof desc.sdp !== 'string') {
+ console.warn('An empty description was passed as an argument.');
+ return desc;
+ }
+
+ var transform = require('sdp-transform');
+ var session = transform.parse(desc.sdp);
+
+ if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
+ Array.isArray(session.media)) {
+ session.media.forEach(function (mLine) {
+
+ // Chrome appears to be picky about the order in which a=ssrc lines
+ // are listed in an m-line when rtx is enabled (and thus there are
+ // a=ssrc-group lines with FID semantics). Specifically if we have
+ // "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
+ // the "a=ssrc:S1" lines, SRD fails.
+ // So, put SSRC which appear as the first SSRC in an FID ssrc-group
+ // first.
+ var firstSsrcs = [];
+ var newSsrcLines = [];
+
+ if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
+ mLine.ssrcGroups.forEach(function (group) {
+ if (typeof group.semantics !== 'undefined' &&
+ group.semantics === 'FID') {
+ if (typeof group.ssrcs !== 'undefined') {
+ firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
+ }
+ }
+ });
+ }
+
+ if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
+ for (var i = 0; i 0) {
+ // start gathering stats
+ }
+ */
+};
+
+TraceablePeerConnection.prototype.setRemoteDescription
+ = function (description, successCallback, failureCallback) {
+ this.trace('setRemoteDescription::preTransform', dumpSDP(description));
+ // TODO the focus should squeze or explode the remote simulcast
+ description = this.simulcast.mungeRemoteDescription(description);
+ this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
+
+ // if we're running on FF, transform to Plan A first.
+ if (RTCBrowserType.usesUnifiedPlan()) {
+ description = this.interop.toUnifiedPlan(description);
+ this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
+ }
+
+ if (RTCBrowserType.usesPlanB()) {
+ description = normalizePlanB(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', dumpSDP(offer));
+ // NOTE this is not tested because in meet the focus generates the
+ // offer.
+
+ // if we're running on FF, transform to Plan B first.
+ if (RTCBrowserType.usesUnifiedPlan()) {
+ offer = self.interop.toPlanB(offer);
+ self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
+ }
+
+ offer = SSRCReplacement.mungeLocalVideoSSRC(offer);
+
+ if (config.enableSimulcast && self.simulcast.isSupported()) {
+ offer = self.simulcast.mungeLocalDescription(offer);
+ self.trace('createOfferOnSuccess::postTransform (simulcast)', 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::preTransform', dumpSDP(answer));
+ // if we're running on FF, transform to Plan A first.
+ if (RTCBrowserType.usesUnifiedPlan()) {
+ answer = self.interop.toPlanB(answer);
+ self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer));
+ }
+
+ // munge local video SSRC
+ answer = SSRCReplacement.mungeLocalVideoSSRC(answer);
+
+ if (config.enableSimulcast && self.simulcast.isSupported()) {
+ answer = self.simulcast.mungeLocalDescription(answer);
+ self.trace('createAnswerOnSuccess::postTransform (simulcast)', 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) {
+ // TODO: Is this the correct way to handle Opera, Temasys?
+ if (RTCBrowserType.isFirefox()) {
+ // ignore for now...
+ if(!errback)
+ errback = function () {};
+ this.peerconnection.getStats(null, callback, errback);
+ } else {
+ this.peerconnection.getStats(callback);
+ }
+};
+
+module.exports = TraceablePeerConnection;
+
+
+},{"../../service/xmpp/XMPPEvents":169,"../RTC/RTC":7,"../RTC/RTCBrowserType.js":8,"./LocalSSRCReplacement":55,"sdp-interop":92,"sdp-simulcast":99,"sdp-transform":106}],60:[function(require,module,exports){
+/* global $, $iq, APP, config, 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);
+ }
+ },
+
+ onMucMemberLeft: 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.startAudioMuted !== undefined)
+ {
+ elem.c(
+ 'property',
+ { name: 'startAudioMuted', value: config.startAudioMuted})
+ .up();
+ }
+ if(config.startVideoMuted !== undefined)
+ {
+ elem.c(
+ 'property',
+ { name: 'startVideoMuted', value: config.startVideoMuted})
+ .up();
+ }
+ elem.c(
+ 'property',
+ { name: 'simulcastMode', value: 'rewriting'})
+ .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) {
+ eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED,
+ focusComponent, 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":164,"../../service/xmpp/XMPPEvents":169,"../settings/Settings":47}],61:[function(require,module,exports){
+/* global $, $iq, config, connection, focusMucJid, messageHandler,
+ 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;
+
+/**
+ * The callback to update the recording button. Currently used from colibri
+ * after receiving a pending status.
+ */
+var recordingStateChangeCallback = null;
+
+function setRecordingToken(token) {
+ recordingToken = token;
+}
+
+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 === 'on') ? 'start' : 'stop',
+ mucjid: connection.emuc.roomjid});
+ if (state === 'off'){
+ 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 === 'on') ? 'started' : 'stopped') +
+ '(jirecon)' + result);
+ recordingEnabled = state;
+ if (state === 'off'){
+ 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 = recordingElem.attr('state');
+
+ recordingEnabled = newState;
+ callback(newState);
+
+ if (newState === 'pending' && recordingStateChangeCallback == null) {
+ recordingStateChangeCallback = callback;
+ connection.addHandler(function(iq){
+ var state = $(iq).find('recording').attr('state');
+ if (state)
+ recordingStateChangeCallback(state);
+ }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
+ }
+ },
+ function (error) {
+ console.warn(error);
+ callback(recordingEnabled);
+ }
+ );
+}
+
+function setRecording(state, token, callback, connection) {
+ if (useJirecon){
+ setRecordingJirecon(state, token, callback, connection);
+ } else {
+ setRecordingColibri(state, token, callback, connection);
+ }
+}
+
+var Recording = {
+ toggleRecording: function (tokenEmptyCallback, recordingStateChangeCallback, 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, recordingStateChangeCallback, connection);
+ });
+
+ return;
+ }
+
+ var oldState = recordingEnabled;
+ var newState = (oldState === 'off' || !oldState) ? 'on' : 'off';
+
+ setRecording(newState,
+ 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);
+ }
+ recordingStateChangeCallback(state);
+
+ },
+ connection
+ );
+ }
+
+};
+
+module.exports = Recording;
+},{"./moderator":60}],62:[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");
+
+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,
+ bridgeIsDown: false,
+ init: function (conn) {
+ this.connection = conn;
+ },
+ initPresenceMap: function (myroomjid) {
+ this.presMap['to'] = myroomjid;
+ this.presMap['xns'] = 'http://jabber.org/protocol/muc';
+ if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted()) {
+ this.addAudioInfoToPresence(true);
+ }
+ if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted()) {
+ this.addVideoInfoToPresence(true);
+ }
+ },
+ 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) {
+ eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
+ }
+ }
+
+ var url;
+ // Parse prezi tag.
+ var presentation = $(pres).find('>prezi');
+ if (presentation.length) {
+ 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) {
+ 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) {
+ eventEmitter.emit(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
+ from, (audioMuted.text() === "true"));
+ }
+
+ // Parse video info tag.
+ var videoMuted = $(pres).find('>videomuted');
+ if (videoMuted.length) {
+ eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_MUTED,
+ from, (videoMuted.text() === "true"));
+ }
+
+ var startMuted = $(pres).find('>startmuted');
+ if (startMuted.length && Moderator.isPeerModerator(from)) {
+ eventEmitter.emit(XMPPEvents.START_MUTED_SETTING_CHANGED,
+ startMuted.attr("audio") === "true",
+ startMuted.attr("video") === "true");
+ }
+
+ var devices = $(pres).find('>devices');
+ if(devices.length)
+ {
+ var audio = devices.find('>audio');
+ var video = devices.find('>video');
+ var devicesValues = {audio: false, video: false};
+ if(audio.length && audio.text() === "true")
+ {
+ devicesValues.audio = true;
+ }
+
+ if(video.length && video.text() === "true")
+ {
+ devicesValues.video = true;
+ }
+ eventEmitter.emit(XMPPEvents.DEVICE_AVAILABLE,
+ Strophe.getResourceFromJid(from), devicesValues);
+ }
+
+ var videoType = $(pres).find('>videoType');
+ if (videoType.length)
+ {
+ if (videoType.text().length)
+ {
+ eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
+ Strophe.getResourceFromJid(from), videoType.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.text() : null);
+
+ if (from == this.myroomjid) {
+ if (member.affiliation == 'owner') this.isOwner = true;
+ if (this.role !== member.role) {
+ this.role = member.role;
+
+ eventEmitter.emit(XMPPEvents.LOCAL_ROLE_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_MEMBER_JOINED, 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();
+ eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
+
+ } else {
+ console.warn('onPresError ', pres);
+ eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
+ }
+ } else {
+ console.warn('onPresError ', pres);
+ eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, 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);
+ }
+ }
+
+ // xep-0203 delay
+ var stamp = $(msg).find('>delay').attr('stamp');
+
+ if (!stamp) {
+ // or xep-0091 delay, UTC timestamp
+ stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
+
+ if (stamp) {
+ // the format is CCYYMMDDThh:mm:ss
+ var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
+ stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
+ }
+ }
+
+ if (txt) {
+ console.log('chat', nick, txt);
+ eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
+ from, nick, txt, this.myroomjid, stamp);
+ }
+ 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 () {
+ if (!this.presMap['to']) {
+ // Too early to send presence - not initialized
+ return;
+ }
+ 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["devices"])
+ {
+ pres.c('devices').c('audio').t(this.presMap['devices'].audio).up()
+ .c('video').t(this.presMap['devices'].video).up().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['videoTypeNs']) {
+ pres.c('videoType', { xmlns: this.presMap['videoTypeNs'] })
+ .t(this.presMap['videoType']).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["startMuted"] !== undefined)
+ {
+ pres.c("startmuted", {audio: this.presMap["startMuted"].audio,
+ video: this.presMap["startMuted"].video,
+ xmlns: "http://jitsi.org/jitmeet/start-muted"});
+ delete this.presMap["startMuted"];
+ }
+
+ pres.up();
+ this.connection.send(pres);
+ },
+ addDisplayNameToPresence: function (displayName) {
+ this.presMap['displayName'] = displayName;
+ },
+ addDevicesToPresence: function (devices) {
+ this.presMap['devices'] = devices;
+ },
+ /**
+ * Adds the info about the type of our video stream.
+ * @param videoType 'camera' or 'screen'
+ */
+ addVideoTypeToPresence: function (videoType) {
+ this.presMap['videoTypeNs'] = 'http://jitsi.org/jitmeet/video';
+ this.presMap['videoType'] = videoType;
+ },
+ 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];
+ },
+ 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;
+ },
+ addStartMutedToPresence: function (audio, video) {
+ this.presMap["startMuted"] = {audio: audio, video: video};
+ },
+ 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_MEMBER_LEFT, jid);
+
+ this.connection.jingle.terminateByJid(jid);
+
+ if (this.getPrezi(jid)) {
+ $(document).trigger('presentationremoved.muc',
+ [jid, this.getPrezi(jid)]);
+ }
+
+ Moderator.onMucMemberLeft(jid);
+ },
+ parsePresence: function (from, member, pres) {
+ if($(pres).find(">bridgeIsDown").length > 0 && !this.bridgeIsDown) {
+ this.bridgeIsDown = true;
+ eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
+ }
+
+ if(member.isFocus)
+ return;
+
+ var displayName = !config.displayJids
+ ? member.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":169,"./moderator":60}],63:[function(require,module,exports){
+/* jshint -W117 */
+
+var JingleSession = require("./JingleSessionPC");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var RTCBrowserType = require("../RTC/RTCBrowserType");
+
+
+module.exports = function(XMPP, eventEmitter) {
+ 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:apps:dtls:0');
+ 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');
+
+ if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()
+ || RTCBrowserType.isTemasysPluginUsed()) {
+ this.connection.disco.addFeature('urn:ietf:rfc:4588');
+ }
+
+ // this is dealt with by SDP O/A so we don't need to announce 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
+
+ this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
+ 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;
+ }
+ // local jid is not checked
+ if (fromJid != 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':
+ var startMuted = $(iq).find('jingle>startmuted');
+ if (startMuted && startMuted.length > 0) {
+ var audioMuted = startMuted.attr("audio");
+ var videoMuted = startMuted.attr("video");
+ eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
+ audioMuted === "true", videoMuted === "true");
+ }
+ sess = new JingleSession(
+ $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
+ this.connection, XMPP, eventEmitter);
+ // configure session
+
+ sess.media_constraints = this.media_constraints;
+ sess.pc_constraints = this.pc_constraints;
+ sess.ice_config = this.ice_config;
+
+ sess.initialize(fromJid, false);
+ // FIXME: setRemoteDescription should only be done when this call is to be accepted
+ sess.setOffer($(iq).find('>jingle'));
+
+ this.sessions[sess.sid] = sess;
+ this.jid2session[sess.peerjid] = sess;
+
+ // the callback should either
+ // .sendAnswer and .accept
+ // or .sendTerminate -- not necessarily synchronous
+
+ // TODO: do we check activecall == null?
+ this.connection.jingle.activecall = sess;
+
+ eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
+
+ // TODO: check affiliation and/or role
+ console.log('emuc data for', sess.peerjid,
+ this.connection.emuc.members[sess.peerjid]);
+ sess.sendAnswer();
+ sess.accept();
+ break;
+ case 'session-accept':
+ sess.setAnswer($(iq).find('>jingle'));
+ 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, eventEmitter);
+ // configure session
+
+ sess.media_constraints = this.media_constraints;
+ sess.pc_constraints = this.pc_constraints;
+ sess.ice_config = this.ice_config;
+
+ sess.initialize(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?
+ },
+
+ /**
+ * Returns the data saved in 'updateLog' in a format to be logged.
+ */
+ getLog: function () {
+ var data = {};
+ var self = this;
+ Object.keys(this.sessions).forEach(function (sid) {
+ var session = self.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":169,"../RTC/RTCBrowserType":8,"./JingleSessionPC":54}],64:[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]);
+ }
+ });
+};
+},{}],65:[function(require,module,exports){
+/* global $, $iq, config, connection, focusMucJid, forceMuted,
+ setAudioMuted, Strophe */
+/**
+ * Moderate connection plugin.
+ */
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+
+module.exports = function (XMPP, eventEmitter) {
+ 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";
+ eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, 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);
+ }
+ });
+}
+},{"../../service/xmpp/XMPPEvents":169}],66:[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;
+ }
+ );
+ }
+ }
+ );
+};
+
+},{}],67:[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";
+ }
+ };
+};
+
+},{}],68:[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 Settings = require("../settings/Settings");
+var Pako = require("pako");
+var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
+var RTCEvents = require("../../service/RTC/RTCEvents");
+var XMPPEvents = require("../../service/xmpp/XMPPEvents");
+var retry = require('retry');
+
+var eventEmitter = new EventEmitter();
+var connection = null;
+var authenticatedUser = false;
+
+function connect(jid, password) {
+
+ var faultTolerantConnect = retry.operation({
+ retries: 3
+ });
+
+ // fault tolerant connect
+ faultTolerantConnect.attempt(function () {
+
+ connection = XMPP.createConnection();
+ Moderator.setConnection(connection);
+
+ 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});
+ }
+
+ // Include user info in MUC presence
+ var settings = Settings.getSettings();
+ if (settings.email) {
+ connection.emuc.addEmailToPresence(settings.email);
+ }
+ if (settings.uid) {
+ connection.emuc.addUserIdToPresence(settings.uid);
+ }
+ if (settings.displayName) {
+ connection.emuc.addDisplayNameToPresence(settings.displayName);
+ }
+
+
+ // connection.connect() starts the connection process.
+ //
+ // As the connection process proceeds, the user supplied callback will
+ // be triggered multiple times with status updates. The callback should
+ // take two arguments - the status code and the error condition.
+ //
+ // The status code will be one of the values in the Strophe.Status
+ // constants. The error condition will be one of the conditions defined
+ // in RFC 3920 or the condition ‘strophe-parsererror’.
+ //
+ // The Parameters wait, hold and route are optional and only relevant
+ // for BOSH connections. Please see XEP 124 for a more detailed
+ // explanation of the optional parameters.
+ //
+ // Connection status constants for use by the connection handler
+ // callback.
+ //
+ // Status.ERROR - An error has occurred (websockets specific)
+ // Status.CONNECTING - The connection is currently being made
+ // Status.CONNFAIL - The connection attempt failed
+ // Status.AUTHENTICATING - The connection is authenticating
+ // Status.AUTHFAIL - The authentication attempt failed
+ // Status.CONNECTED - The connection has succeeded
+ // Status.DISCONNECTED - The connection has been terminated
+ // Status.DISCONNECTING - The connection is currently being terminated
+ // Status.ATTACHED - The connection has been attached
+
+ var anonymousConnectionFailed = false;
+ var connectionFailed = false;
+ var lastErrorMsg;
+ connection.connect(jid, password, function (status, msg) {
+ console.log('Strophe status changed to',
+ Strophe.getStatusString(status), msg);
+ 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 {
+ connectionFailed = true;
+ }
+ lastErrorMsg = msg;
+ } else if (status === Strophe.Status.DISCONNECTED) {
+ if (anonymousConnectionFailed) {
+ // prompt user for username and password
+ XMPP.promptLogin();
+ } else {
+
+ // Strophe already has built-in HTTP/BOSH error handling and
+ // request retry logic. Requests are resent automatically
+ // until their error count reaches 5. Strophe.js disconnects
+ // if the error count is > 5. We are not replicating this
+ // here.
+ //
+ // The "problem" is that failed HTTP/BOSH requests don't
+ // trigger a callback with a status update, so when a
+ // callback with status Strophe.Status.DISCONNECTED arrives,
+ // we can't be sure if it's a graceful disconnect or if it's
+ // triggered by some HTTP/BOSH error.
+ //
+ // But that's a minor issue in Jitsi Meet as we never
+ // disconnect anyway, not even when the user closes the
+ // browser window (which is kind of wrong, but the point is
+ // that we should never ever get disconnected).
+ //
+ // On the other hand, failed connections due to XMPP layer
+ // errors, trigger a callback with status Strophe.Status.CONNFAIL.
+ //
+ // Here we implement retry logic for failed connections due
+ // to XMPP layer errors and we display an error to the user
+ // if we get disconnected from the XMPP server permanently.
+
+ // If the connection failed, retry.
+ if (connectionFailed &&
+ faultTolerantConnect.retry("connection-failed")) {
+ return;
+ }
+
+ // If we failed to connect to the XMPP server, fire an event
+ // to let all the interested module now about it.
+ eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
+ msg ? msg : lastErrorMsg);
+ }
+ } 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() {
+ eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
+}
+
+function initStrophePlugins()
+{
+ require("./strophe.emuc")(XMPP, eventEmitter);
+ require("./strophe.jingle")(XMPP, eventEmitter);
+ require("./strophe.moderate")(XMPP, eventEmitter);
+ require("./strophe.util")();
+ require("./strophe.rayo")();
+ require("./strophe.logger")();
+}
+
+/**
+ * If given localStream is video one this method will advertise it's
+ * video type in MUC presence.
+ * @param localStream new or modified LocalStream .
+ */
+function broadcastLocalVideoType(localStream) {
+ if (localStream.videoType)
+ XMPP.addToPresence('videoType', localStream.videoType);
+}
+
+function registerListeners() {
+ APP.RTC.addStreamListener(
+ function (localStream) {
+ maybeDoJoin();
+ broadcastLocalVideoType(localStream);
+ },
+ StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
+ );
+ APP.RTC.addStreamListener(
+ broadcastLocalVideoType,
+ StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
+ );
+ APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
+ XMPP.addToPresence("devices", devices);
+ });
+}
+
+var unload = (function () {
+ var unloaded = false;
+
+ return function () {
+ if (unloaded) { return; }
+ unloaded = true;
+
+ 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);
+ };
+})();
+
+function setupEvents() {
+ // In recent versions of FF the 'beforeunload' event is not fired when the
+ // window or the tab is closed. It is only fired when we leave the page
+ // (change URL). If this participant doesn't unload properly, then it
+ // becomes a ghost for the rest of the participants that stay in the
+ // conference. Thankfully handling the 'unload' event in addition to the
+ // 'beforeunload' event seems to guarantee the execution of the 'unload'
+ // method at least once.
+ //
+ // The 'unload' method can safely be run multiple times, it will actually do
+ // something only the first time that it's run, so we're don't have to worry
+ // about browsers that fire both events.
+
+ $(window).bind('beforeunload', unload);
+ $(window).bind('unload', unload);
+}
+
+var XMPP = {
+ getConnection: function(){ return connection; },
+ 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 () {
+ eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN);
+ },
+ joinRoom: function(roomName, useNicks, nick) {
+ var 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) {
+ 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();
+ }
+ eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
+ 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();
+ },
+ isConferenceInProgress: function () {
+ return connection && connection.jingle.activecall &&
+ connection.jingle.activecall.peerconnection;
+ },
+ switchStreams: function (stream, oldStream, callback, isAudio) {
+ if (this.isConferenceInProgress()) {
+ // FIXME: will block switchInProgress on true value in case of exception
+ connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
+ } else {
+ // We are done immediately
+ console.warn("No conference handler or conference not started yet");
+ callback();
+ }
+ },
+ sendVideoInfoPresence: function (mute) {
+ if(!connection)
+ return;
+ connection.emuc.addVideoInfoToPresence(mute);
+ connection.emuc.sendPresence();
+ },
+ setVideoMute: function (mute, callback, options) {
+ if(!connection)
+ return;
+ var self = this;
+ var localCallback = function (mute) {
+ self.sendVideoInfoPresence(mute);
+ return callback(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;
+ }
+
+ APP.RTC.localAudio.setMute(mute);
+ this.sendAudioInfoPresence(mute, callback);
+ return true;
+ },
+ sendAudioInfoPresence: function(mute, callback) {
+ if(connection) {
+ connection.emuc.addAudioInfoToPresence(mute);
+ connection.emuc.sendPresence();
+ }
+ callback();
+ return true;
+ },
+ toggleRecording: function (tokenEmptyCallback,
+ recordingStateChangeCallback) {
+ Recording.toggleRecording(tokenEmptyCallback,
+ recordingStateChangeCallback, connection);
+ },
+ addToPresence: function (name, value, dontSend) {
+ switch (name) {
+ case "displayName":
+ connection.emuc.addDisplayNameToPresence(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);
+ break;
+ case "devices":
+ connection.emuc.addDevicesToPresence(value);
+ break;
+ case "videoType":
+ connection.emuc.addVideoTypeToPresence(value);
+ break;
+ case "startMuted":
+ if(!Moderator.isModerator())
+ return;
+ connection.emuc.addStartMutedToPresence(value[0],
+ value[1]);
+ break;
+ default :
+ console.log("Unknown tag for presence: " + name);
+ 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;
+ },
+ // Gets the logs from strophe.jingle.
+ getJingleLog: function () {
+ return connection.jingle ? connection.jingle.getLog() : {};
+ },
+ // Gets the logs from strophe.
+ getXmppLog: function () {
+ return connection.logger ? connection.logger.log : 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 (!this.isConferenceInProgress())
+ return null;
+ return connection.jingle.activecall.getSsrcOwner(ssrc);
+ },
+ // Returns true iff we have joined the MUC.
+ isMUCJoined: function () {
+ return connection.emuc.joined;
+ },
+ getSessions: function () {
+ return connection.jingle.sessions;
+ },
+ removeStream: function (stream) {
+ if (!this.isConferenceInProgress())
+ return;
+ connection.jingle.activecall.peerconnection.removeStream(stream);
+ }
+};
+
+module.exports = XMPP;
+
+},{"../../service/RTC/RTCEvents":160,"../../service/RTC/StreamEventTypes":162,"../../service/xmpp/XMPPEvents":169,"../settings/Settings":47,"./SDP":56,"./moderator":60,"./recording":61,"./strophe.emuc":62,"./strophe.jingle":63,"./strophe.logger":64,"./strophe.moderate":65,"./strophe.rayo":66,"./strophe.util":67,"events":170,"pako":72,"retry":88}],69:[function(require,module,exports){
(function (process){
/*!
* async
@@ -20452,7 +20089,7 @@ module.exports = XMPP;
}());
}).call(this,require('_process'))
-},{"_process":2}],72:[function(require,module,exports){
+},{"_process":171}],70:[function(require,module,exports){
// i18next, v1.7.7
// Copyright (c)2014 Jan Mühlemann (jamuhl).
// Distributed under MIT license
@@ -22575,7 +22212,7 @@ module.exports = XMPP;
i18n.options = o;
})();
-},{"jquery":"jquery"}],73:[function(require,module,exports){
+},{"jquery":"jquery"}],71:[function(require,module,exports){
/*
A JavaScript implementation of the SHA family of hashes, as
defined in FIPS PUB 180-2 as well as the corresponding HMAC implementation
@@ -22611,7 +22248,7 @@ d[2]),new e(355462360,d[3]),new e(1731405415,d[4]),new e(41048885895,d[5]),new e
new e(a[m*x+p],a[m*x+p+1]):y(E(A[m-2]),A[m-7],D(A[m-15]),A[m-16]),v=C(u,G(l),H(l,r,t),k[m],A[m]),z=q(F(c),I(c,g,f)),u=t,t=r,r=l,l=q(h,v),h=f,f=g,g=c,c=q(v,z);d[0]=q(c,d[0]);d[1]=q(g,d[1]);d[2]=q(f,d[2]);d[3]=q(h,d[3]);d[4]=q(l,d[4]);d[5]=q(r,d[5]);d[6]=q(t,d[6]);d[7]=q(u,d[7])}if("SHA-224"===b)a=[d[0],d[1],d[2],d[3],d[4],d[5],d[6]];else if("SHA-256"===b)a=d;else if("SHA-384"===b)a=[d[0].a,d[0].b,d[1].a,d[1].b,d[2].a,d[2].b,d[3].a,d[3].b,d[4].a,d[4].b,d[5].a,d[5].b];else if("SHA-512"===b)a=[d[0].a,
d[0].b,d[1].a,d[1].b,d[2].a,d[2].b,d[3].a,d[3].b,d[4].a,d[4].b,d[5].a,d[5].b,d[6].a,d[6].b,d[7].a,d[7].b];else throw"Unexpected error in SHA-2 implementation";return a}"function"===typeof define&&typeof define.amd?define(function(){return z}):"undefined"!==typeof exports?"undefined"!==typeof module&&module.exports?module.exports=exports=z:exports=z:T.jsSHA=z})(this);
-},{}],74:[function(require,module,exports){
+},{}],72:[function(require,module,exports){
// Top level file is just a mixin of submodules & constants
'use strict';
@@ -22627,7 +22264,7 @@ assign(pako, deflate, inflate, constants);
module.exports = pako;
-},{"./lib/deflate":75,"./lib/inflate":76,"./lib/utils/common":77,"./lib/zlib/constants":80}],75:[function(require,module,exports){
+},{"./lib/deflate":73,"./lib/inflate":74,"./lib/utils/common":75,"./lib/zlib/constants":78}],73:[function(require,module,exports){
'use strict';
@@ -23005,7 +22642,7 @@ exports.deflate = deflate;
exports.deflateRaw = deflateRaw;
exports.gzip = gzip;
-},{"./utils/common":77,"./utils/strings":78,"./zlib/deflate.js":82,"./zlib/messages":87,"./zlib/zstream":89}],76:[function(require,module,exports){
+},{"./utils/common":75,"./utils/strings":76,"./zlib/deflate.js":80,"./zlib/messages":85,"./zlib/zstream":87}],74:[function(require,module,exports){
'use strict';
@@ -23386,7 +23023,7 @@ exports.inflate = inflate;
exports.inflateRaw = inflateRaw;
exports.ungzip = inflate;
-},{"./utils/common":77,"./utils/strings":78,"./zlib/constants":80,"./zlib/gzheader":83,"./zlib/inflate.js":85,"./zlib/messages":87,"./zlib/zstream":89}],77:[function(require,module,exports){
+},{"./utils/common":75,"./utils/strings":76,"./zlib/constants":78,"./zlib/gzheader":81,"./zlib/inflate.js":83,"./zlib/messages":85,"./zlib/zstream":87}],75:[function(require,module,exports){
'use strict';
@@ -23490,7 +23127,7 @@ exports.setTyped = function (on) {
exports.setTyped(TYPED_OK);
-},{}],78:[function(require,module,exports){
+},{}],76:[function(require,module,exports){
// String encode/decode helpers
'use strict';
@@ -23677,7 +23314,7 @@ exports.utf8border = function(buf, max) {
return (pos + _utf8len[buf[pos]] > max) ? pos : max;
};
-},{"./common":77}],79:[function(require,module,exports){
+},{"./common":75}],77:[function(require,module,exports){
'use strict';
// Note: adler32 takes 12% for level 0 and 2% for level 6.
@@ -23711,7 +23348,7 @@ function adler32(adler, buf, len, pos) {
module.exports = adler32;
-},{}],80:[function(require,module,exports){
+},{}],78:[function(require,module,exports){
module.exports = {
/* Allowed flush values; see deflate() and inflate() below for details */
@@ -23760,7 +23397,7 @@ module.exports = {
//Z_NULL: null // Use -1 or null inline, depending on var type
};
-},{}],81:[function(require,module,exports){
+},{}],79:[function(require,module,exports){
'use strict';
// Note: we can't get significant speed boost here.
@@ -23803,7 +23440,7 @@ function crc32(crc, buf, len, pos) {
module.exports = crc32;
-},{}],82:[function(require,module,exports){
+},{}],80:[function(require,module,exports){
'use strict';
var utils = require('../utils/common');
@@ -25570,7 +25207,7 @@ exports.deflatePrime = deflatePrime;
exports.deflateTune = deflateTune;
*/
-},{"../utils/common":77,"./adler32":79,"./crc32":81,"./messages":87,"./trees":88}],83:[function(require,module,exports){
+},{"../utils/common":75,"./adler32":77,"./crc32":79,"./messages":85,"./trees":86}],81:[function(require,module,exports){
'use strict';
@@ -25612,7 +25249,7 @@ function GZheader() {
module.exports = GZheader;
-},{}],84:[function(require,module,exports){
+},{}],82:[function(require,module,exports){
'use strict';
// See state defs from inflate.js
@@ -25939,7 +25576,7 @@ module.exports = function inflate_fast(strm, start) {
return;
};
-},{}],85:[function(require,module,exports){
+},{}],83:[function(require,module,exports){
'use strict';
@@ -27444,7 +27081,7 @@ exports.inflateSyncPoint = inflateSyncPoint;
exports.inflateUndermine = inflateUndermine;
*/
-},{"../utils/common":77,"./adler32":79,"./crc32":81,"./inffast":84,"./inftrees":86}],86:[function(require,module,exports){
+},{"../utils/common":75,"./adler32":77,"./crc32":79,"./inffast":82,"./inftrees":84}],84:[function(require,module,exports){
'use strict';
@@ -27773,7 +27410,7 @@ module.exports = function inflate_table(type, lens, lens_index, codes, table, ta
return 0;
};
-},{"../utils/common":77}],87:[function(require,module,exports){
+},{"../utils/common":75}],85:[function(require,module,exports){
'use strict';
module.exports = {
@@ -27788,7 +27425,7 @@ module.exports = {
'-6': 'incompatible version' /* Z_VERSION_ERROR (-6) */
};
-},{}],88:[function(require,module,exports){
+},{}],86:[function(require,module,exports){
'use strict';
@@ -28989,7 +28626,7 @@ exports._tr_flush_block = _tr_flush_block;
exports._tr_tally = _tr_tally;
exports._tr_align = _tr_align;
-},{"../utils/common":77}],89:[function(require,module,exports){
+},{"../utils/common":75}],87:[function(require,module,exports){
'use strict';
@@ -29020,9 +28657,9 @@ function ZStream() {
module.exports = ZStream;
-},{}],90:[function(require,module,exports){
+},{}],88:[function(require,module,exports){
module.exports = require('./lib/retry');
-},{"./lib/retry":91}],91:[function(require,module,exports){
+},{"./lib/retry":89}],89:[function(require,module,exports){
var RetryOperation = require('./retry_operation');
exports.operation = function(options) {
@@ -29073,7 +28710,7 @@ exports._createTimeout = function(attempt, opts) {
return timeout;
};
-},{"./retry_operation":92}],92:[function(require,module,exports){
+},{"./retry_operation":90}],90:[function(require,module,exports){
function RetryOperation(timeouts) {
this._timeouts = timeouts;
this._fn = null;
@@ -29183,7 +28820,7 @@ RetryOperation.prototype.mainError = function() {
return mainError;
};
-},{}],93:[function(require,module,exports){
+},{}],91:[function(require,module,exports){
module.exports = function arrayEquals(array) {
// if the other array is a falsy value, return
if (!array)
@@ -29209,10 +28846,10 @@ module.exports = function arrayEquals(array) {
}
-},{}],94:[function(require,module,exports){
+},{}],92:[function(require,module,exports){
exports.Interop = require('./interop');
-},{"./interop":95}],95:[function(require,module,exports){
+},{"./interop":93}],93:[function(require,module,exports){
"use strict";
var transform = require('./transform');
@@ -29794,7 +29431,7 @@ Interop.prototype.toUnifiedPlan = function(desc) {
//#endregion
};
-},{"./array-equals":93,"./transform":96}],96:[function(require,module,exports){
+},{"./array-equals":91,"./transform":94}],94:[function(require,module,exports){
var transform = require('sdp-transform');
exports.write = function(session, opts) {
@@ -29893,7 +29530,477 @@ exports.parse = function(sdp) {
};
-},{"sdp-transform":100}],97:[function(require,module,exports){
+},{"sdp-transform":96}],95:[function(require,module,exports){
+var grammar = module.exports = {
+ v: [{
+ name: 'version',
+ reg: /^(\d*)$/
+ }],
+ o: [{ //o=- 20518 0 IN IP4 203.0.113.1
+ // NB: sessionId will be a String in most cases because it is huge
+ name: 'origin',
+ reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
+ names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
+ format: "%s %s %d %s IP%d %s"
+ }],
+ // default parsing of these only (though some of these feel outdated)
+ s: [{ name: 'name' }],
+ i: [{ name: 'description' }],
+ u: [{ name: 'uri' }],
+ e: [{ name: 'email' }],
+ p: [{ name: 'phone' }],
+ z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
+ r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly
+ //k: [{}], // outdated thing ignored
+ t: [{ //t=0 0
+ name: 'timing',
+ reg: /^(\d*) (\d*)/,
+ names: ['start', 'stop'],
+ format: "%d %d"
+ }],
+ c: [{ //c=IN IP4 10.47.197.26
+ name: 'connection',
+ reg: /^IN IP(\d) (\S*)/,
+ names: ['version', 'ip'],
+ format: "IN IP%d %s"
+ }],
+ b: [{ //b=AS:4000
+ push: 'bandwidth',
+ reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
+ names: ['type', 'limit'],
+ format: "%s:%s"
+ }],
+ m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
+ // NB: special - pushes to session
+ // TODO: rtp/fmtp should be filtered by the payloads found here?
+ reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
+ names: ['type', 'port', 'protocol', 'payloads'],
+ format: "%s %d %s %s"
+ }],
+ a: [
+ { //a=rtpmap:110 opus/48000/2
+ push: 'rtp',
+ reg: /^rtpmap:(\d*) ([\w\-]*)\/(\d*)(?:\s*\/(\S*))?/,
+ names: ['payload', 'codec', 'rate', 'encoding'],
+ format: function (o) {
+ return (o.encoding) ?
+ "rtpmap:%d %s/%s/%s":
+ "rtpmap:%d %s/%s";
+ }
+ },
+ { //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
+ push: 'fmtp',
+ reg: /^fmtp:(\d*) (\S*)/,
+ names: ['payload', 'config'],
+ format: "fmtp:%d %s"
+ },
+ { //a=control:streamid=0
+ name: 'control',
+ reg: /^control:(.*)/,
+ format: "control:%s"
+ },
+ { //a=rtcp:65179 IN IP4 193.84.77.194
+ name: 'rtcp',
+ reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
+ names: ['port', 'netType', 'ipVer', 'address'],
+ format: function (o) {
+ return (o.address != null) ?
+ "rtcp:%d %s IP%d %s":
+ "rtcp:%d";
+ }
+ },
+ { //a=rtcp-fb:98 trr-int 100
+ push: 'rtcpFbTrrInt',
+ reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
+ names: ['payload', 'value'],
+ format: "rtcp-fb:%d trr-int %d"
+ },
+ { //a=rtcp-fb:98 nack rpsi
+ push: 'rtcpFb',
+ reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
+ names: ['payload', 'type', 'subtype'],
+ format: function (o) {
+ return (o.subtype != null) ?
+ "rtcp-fb:%s %s %s":
+ "rtcp-fb:%s %s";
+ }
+ },
+ { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+ //a=extmap:1/recvonly URI-gps-string
+ push: 'ext',
+ reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
+ names: ['value', 'uri', 'config'], // value may include "/direction" suffix
+ format: function (o) {
+ return (o.config != null) ?
+ "extmap:%s %s %s":
+ "extmap:%s %s";
+ }
+ },
+ {
+ //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
+ push: 'crypto',
+ reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
+ names: ['id', 'suite', 'config', 'sessionConfig'],
+ format: function (o) {
+ return (o.sessionConfig != null) ?
+ "crypto:%d %s %s %s":
+ "crypto:%d %s %s";
+ }
+ },
+ { //a=setup:actpass
+ name: 'setup',
+ reg: /^setup:(\w*)/,
+ format: "setup:%s"
+ },
+ { //a=mid:1
+ name: 'mid',
+ reg: /^mid:([^\s]*)/,
+ format: "mid:%s"
+ },
+ { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
+ name: 'msid',
+ reg: /^msid:(.*)/,
+ format: "msid:%s"
+ },
+ { //a=ptime:20
+ name: 'ptime',
+ reg: /^ptime:(\d*)/,
+ format: "ptime:%d"
+ },
+ { //a=maxptime:60
+ name: 'maxptime',
+ reg: /^maxptime:(\d*)/,
+ format: "maxptime:%d"
+ },
+ { //a=sendrecv
+ name: 'direction',
+ reg: /^(sendrecv|recvonly|sendonly|inactive)/
+ },
+ { //a=ice-lite
+ name: 'icelite',
+ reg: /^(ice-lite)/
+ },
+ { //a=ice-ufrag:F7gI
+ name: 'iceUfrag',
+ reg: /^ice-ufrag:(\S*)/,
+ format: "ice-ufrag:%s"
+ },
+ { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
+ name: 'icePwd',
+ reg: /^ice-pwd:(\S*)/,
+ format: "ice-pwd:%s"
+ },
+ { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
+ name: 'fingerprint',
+ reg: /^fingerprint:(\S*) (\S*)/,
+ names: ['type', 'hash'],
+ format: "fingerprint:%s %s"
+ },
+ {
+ //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
+ //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
+ //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
+ push:'candidates',
+ reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: generation (\d*))?/,
+ names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'generation'],
+ format: function (o) {
+ var str = "candidate:%s %d %s %d %s %d typ %s";
+ // NB: candidate has two optional chunks, so %void middle one if it's missing
+ str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
+ if (o.generation != null) {
+ str += " generation %d";
+ }
+ return str;
+ }
+ },
+ { //a=end-of-candidates (keep after the candidates line for readability)
+ name: 'endOfCandidates',
+ reg: /^(end-of-candidates)/
+ },
+ { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
+ name: 'remoteCandidates',
+ reg: /^remote-candidates:(.*)/,
+ format: "remote-candidates:%s"
+ },
+ { //a=ice-options:google-ice
+ name: 'iceOptions',
+ reg: /^ice-options:(\S*)/,
+ format: "ice-options:%s"
+ },
+ { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
+ push: "ssrcs",
+ reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
+ names: ['id', 'attribute', 'value'],
+ format: "ssrc:%d %s:%s"
+ },
+ { //a=ssrc-group:FEC 1 2
+ push: "ssrcGroups",
+ reg: /^ssrc-group:(\w*) (.*)/,
+ names: ['semantics', 'ssrcs'],
+ format: "ssrc-group:%s %s"
+ },
+ { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
+ name: "msidSemantic",
+ reg: /^msid-semantic:\s?(\w*) (\S*)/,
+ names: ['semantic', 'token'],
+ format: "msid-semantic: %s %s" // space after ":" is not accidental
+ },
+ { //a=group:BUNDLE audio video
+ push: 'groups',
+ reg: /^group:(\w*) (.*)/,
+ names: ['type', 'mids'],
+ format: "group:%s %s"
+ },
+ { //a=rtcp-mux
+ name: 'rtcpMux',
+ reg: /^(rtcp-mux)/
+ },
+ { //a=rtcp-rsize
+ name: 'rtcpRsize',
+ reg: /^(rtcp-rsize)/
+ },
+ { // any a= that we don't understand is kepts verbatim on media.invalid
+ push: 'invalid',
+ names: ["value"]
+ }
+ ]
+};
+
+// set sensible defaults to avoid polluting the grammar with boring details
+Object.keys(grammar).forEach(function (key) {
+ var objs = grammar[key];
+ objs.forEach(function (obj) {
+ if (!obj.reg) {
+ obj.reg = /(.*)/;
+ }
+ if (!obj.format) {
+ obj.format = "%s";
+ }
+ });
+});
+
+},{}],96:[function(require,module,exports){
+var parser = require('./parser');
+var writer = require('./writer');
+
+exports.write = writer;
+exports.parse = parser.parse;
+exports.parseFmtpConfig = parser.parseFmtpConfig;
+exports.parsePayloads = parser.parsePayloads;
+exports.parseRemoteCandidates = parser.parseRemoteCandidates;
+
+},{"./parser":97,"./writer":98}],97:[function(require,module,exports){
+var toIntIfInt = function (v) {
+ return String(Number(v)) === v ? Number(v) : v;
+};
+
+var attachProperties = function (match, location, names, rawName) {
+ if (rawName && !names) {
+ location[rawName] = toIntIfInt(match[1]);
+ }
+ else {
+ for (var i = 0; i < names.length; i += 1) {
+ if (match[i+1] != null) {
+ location[names[i]] = toIntIfInt(match[i+1]);
+ }
+ }
+ }
+};
+
+var parseReg = function (obj, location, content) {
+ var needsBlank = obj.name && obj.names;
+ if (obj.push && !location[obj.push]) {
+ location[obj.push] = [];
+ }
+ else if (needsBlank && !location[obj.name]) {
+ location[obj.name] = {};
+ }
+ var keyLocation = obj.push ?
+ {} : // blank object that will be pushed
+ needsBlank ? location[obj.name] : location; // otherwise, named location or root
+
+ attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
+
+ if (obj.push) {
+ location[obj.push].push(keyLocation);
+ }
+};
+
+var grammar = require('./grammar');
+var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
+
+exports.parse = function (sdp) {
+ var session = {}
+ , media = []
+ , location = session; // points at where properties go under (one of the above)
+
+ // parse lines we understand
+ sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
+ var type = l[0];
+ var content = l.slice(2);
+ if (type === 'm') {
+ media.push({rtp: [], fmtp: []});
+ location = media[media.length-1]; // point at latest media line
+ }
+
+ for (var j = 0; j < (grammar[type] || []).length; j += 1) {
+ var obj = grammar[type][j];
+ if (obj.reg.test(content)) {
+ return parseReg(obj, location, content);
+ }
+ }
+ });
+
+ session.media = media; // link it up
+ return session;
+};
+
+var fmtpReducer = function (acc, expr) {
+ var s = expr.split('=');
+ if (s.length === 2) {
+ acc[s[0]] = toIntIfInt(s[1]);
+ }
+ return acc;
+};
+
+exports.parseFmtpConfig = function (str) {
+ return str.split(';').reduce(fmtpReducer, {});
+};
+
+exports.parsePayloads = function (str) {
+ return str.split(' ').map(Number);
+};
+
+exports.parseRemoteCandidates = function (str) {
+ var candidates = [];
+ var parts = str.split(' ').map(toIntIfInt);
+ for (var i = 0; i < parts.length; i += 3) {
+ candidates.push({
+ component: parts[i],
+ ip: parts[i + 1],
+ port: parts[i + 2]
+ });
+ }
+ return candidates;
+};
+
+},{"./grammar":95}],98:[function(require,module,exports){
+var grammar = require('./grammar');
+
+// customized util.format - discards excess arguments and can void middle ones
+var formatRegExp = /%[sdv%]/g;
+var format = function (formatStr) {
+ var i = 1;
+ var args = arguments;
+ var len = args.length;
+ return formatStr.replace(formatRegExp, function (x) {
+ if (i >= len) {
+ return x; // missing argument
+ }
+ var arg = args[i];
+ i += 1;
+ switch (x) {
+ case '%%':
+ return '%';
+ case '%s':
+ return String(arg);
+ case '%d':
+ return Number(arg);
+ case '%v':
+ return '';
+ }
+ });
+ // NB: we discard excess arguments - they are typically undefined from makeLine
+};
+
+var makeLine = function (type, obj, location) {
+ var str = obj.format instanceof Function ?
+ (obj.format(obj.push ? location : location[obj.name])) :
+ obj.format;
+
+ var args = [type + '=' + str];
+ if (obj.names) {
+ for (var i = 0; i < obj.names.length; i += 1) {
+ var n = obj.names[i];
+ if (obj.name) {
+ args.push(location[obj.name][n]);
+ }
+ else { // for mLine and push attributes
+ args.push(location[obj.names[i]]);
+ }
+ }
+ }
+ else {
+ args.push(location[obj.name]);
+ }
+ return format.apply(null, args);
+};
+
+// RFC specified order
+// TODO: extend this with all the rest
+var defaultOuterOrder = [
+ 'v', 'o', 's', 'i',
+ 'u', 'e', 'p', 'c',
+ 'b', 't', 'r', 'z', 'a'
+];
+var defaultInnerOrder = ['i', 'c', 'b', 'a'];
+
+
+module.exports = function (session, opts) {
+ opts = opts || {};
+ // ensure certain properties exist
+ if (session.version == null) {
+ session.version = 0; // "v=0" must be there (only defined version atm)
+ }
+ if (session.name == null) {
+ session.name = " "; // "s= " must be there if no meaningful name set
+ }
+ session.media.forEach(function (mLine) {
+ if (mLine.payloads == null) {
+ mLine.payloads = "";
+ }
+ });
+
+ var outerOrder = opts.outerOrder || defaultOuterOrder;
+ var innerOrder = opts.innerOrder || defaultInnerOrder;
+ var sdp = [];
+
+ // loop through outerOrder for matching properties on session
+ outerOrder.forEach(function (type) {
+ grammar[type].forEach(function (obj) {
+ if (obj.name in session && session[obj.name] != null) {
+ sdp.push(makeLine(type, obj, session));
+ }
+ else if (obj.push in session && session[obj.push] != null) {
+ session[obj.push].forEach(function (el) {
+ sdp.push(makeLine(type, obj, el));
+ });
+ }
+ });
+ });
+
+ // then for each media line, follow the innerOrder
+ session.media.forEach(function (mLine) {
+ sdp.push(makeLine('m', grammar.m[0], mLine));
+
+ innerOrder.forEach(function (type) {
+ grammar[type].forEach(function (obj) {
+ if (obj.name in mLine && mLine[obj.name] != null) {
+ sdp.push(makeLine(type, obj, mLine));
+ }
+ else if (obj.push in mLine && mLine[obj.push] != null) {
+ mLine[obj.push].forEach(function (el) {
+ sdp.push(makeLine(type, obj, el));
+ });
+ }
+ });
+ });
+ });
+
+ return sdp.join('\r\n') + '\r\n';
+};
+
+},{"./grammar":95}],99:[function(require,module,exports){
var transform = require('sdp-transform');
var transformUtils = require('./transform-utils');
var parseSsrcs = transformUtils.parseSsrcs;
@@ -30295,7 +30402,7 @@ Simulcast.prototype.mungeLocalDescription = function (desc) {
module.exports = Simulcast;
-},{"./transform-utils":98,"sdp-transform":100}],98:[function(require,module,exports){
+},{"./transform-utils":100,"sdp-transform":102}],100:[function(require,module,exports){
exports.writeSsrcs = function(sources, order) {
var ssrcs = [];
@@ -30346,7 +30453,15 @@ exports.parseSsrcs = function (mLine) {
};
-},{}],99:[function(require,module,exports){
+},{}],101:[function(require,module,exports){
+arguments[4][95][0].apply(exports,arguments)
+},{"dup":95}],102:[function(require,module,exports){
+arguments[4][96][0].apply(exports,arguments)
+},{"./parser":103,"./writer":104,"dup":96}],103:[function(require,module,exports){
+arguments[4][97][0].apply(exports,arguments)
+},{"./grammar":101,"dup":97}],104:[function(require,module,exports){
+arguments[4][98][0].apply(exports,arguments)
+},{"./grammar":101,"dup":98}],105:[function(require,module,exports){
var grammar = module.exports = {
v: [{
name: 'version',
@@ -30597,17 +30712,9 @@ Object.keys(grammar).forEach(function (key) {
});
});
-},{}],100:[function(require,module,exports){
-var parser = require('./parser');
-var writer = require('./writer');
-
-exports.write = writer;
-exports.parse = parser.parse;
-exports.parseFmtpConfig = parser.parseFmtpConfig;
-exports.parsePayloads = parser.parsePayloads;
-exports.parseRemoteCandidates = parser.parseRemoteCandidates;
-
-},{"./parser":101,"./writer":102}],101:[function(require,module,exports){
+},{}],106:[function(require,module,exports){
+arguments[4][96][0].apply(exports,arguments)
+},{"./parser":107,"./writer":108,"dup":96}],107:[function(require,module,exports){
var toIntIfInt = function (v) {
return String(Number(v)) === v ? Number(v) : v;
};
@@ -30702,127 +30809,13 @@ exports.parseRemoteCandidates = function (str) {
return candidates;
};
-},{"./grammar":99}],102:[function(require,module,exports){
-var grammar = require('./grammar');
-
-// customized util.format - discards excess arguments and can void middle ones
-var formatRegExp = /%[sdv%]/g;
-var format = function (formatStr) {
- var i = 1;
- var args = arguments;
- var len = args.length;
- return formatStr.replace(formatRegExp, function (x) {
- if (i >= len) {
- return x; // missing argument
- }
- var arg = args[i];
- i += 1;
- switch (x) {
- case '%%':
- return '%';
- case '%s':
- return String(arg);
- case '%d':
- return Number(arg);
- case '%v':
- return '';
- }
- });
- // NB: we discard excess arguments - they are typically undefined from makeLine
-};
-
-var makeLine = function (type, obj, location) {
- var str = obj.format instanceof Function ?
- (obj.format(obj.push ? location : location[obj.name])) :
- obj.format;
-
- var args = [type + '=' + str];
- if (obj.names) {
- for (var i = 0; i < obj.names.length; i += 1) {
- var n = obj.names[i];
- if (obj.name) {
- args.push(location[obj.name][n]);
- }
- else { // for mLine and push attributes
- args.push(location[obj.names[i]]);
- }
- }
- }
- else {
- args.push(location[obj.name]);
- }
- return format.apply(null, args);
-};
-
-// RFC specified order
-// TODO: extend this with all the rest
-var defaultOuterOrder = [
- 'v', 'o', 's', 'i',
- 'u', 'e', 'p', 'c',
- 'b', 't', 'r', 'z', 'a'
-];
-var defaultInnerOrder = ['i', 'c', 'b', 'a'];
-
-
-module.exports = function (session, opts) {
- opts = opts || {};
- // ensure certain properties exist
- if (session.version == null) {
- session.version = 0; // "v=0" must be there (only defined version atm)
- }
- if (session.name == null) {
- session.name = " "; // "s= " must be there if no meaningful name set
- }
- session.media.forEach(function (mLine) {
- if (mLine.payloads == null) {
- mLine.payloads = "";
- }
- });
-
- var outerOrder = opts.outerOrder || defaultOuterOrder;
- var innerOrder = opts.innerOrder || defaultInnerOrder;
- var sdp = [];
-
- // loop through outerOrder for matching properties on session
- outerOrder.forEach(function (type) {
- grammar[type].forEach(function (obj) {
- if (obj.name in session && session[obj.name] != null) {
- sdp.push(makeLine(type, obj, session));
- }
- else if (obj.push in session && session[obj.push] != null) {
- session[obj.push].forEach(function (el) {
- sdp.push(makeLine(type, obj, el));
- });
- }
- });
- });
-
- // then for each media line, follow the innerOrder
- session.media.forEach(function (mLine) {
- sdp.push(makeLine('m', grammar.m[0], mLine));
-
- innerOrder.forEach(function (type) {
- grammar[type].forEach(function (obj) {
- if (obj.name in mLine && mLine[obj.name] != null) {
- sdp.push(makeLine(type, obj, mLine));
- }
- else if (obj.push in mLine && mLine[obj.push] != null) {
- mLine[obj.push].forEach(function (el) {
- sdp.push(makeLine(type, obj, el));
- });
- }
- });
- });
- });
-
- return sdp.join('\r\n') + '\r\n';
-};
-
-},{"./grammar":99}],103:[function(require,module,exports){
+},{"./grammar":105}],108:[function(require,module,exports){
+arguments[4][98][0].apply(exports,arguments)
+},{"./grammar":105,"dup":98}],109:[function(require,module,exports){
module.exports = require('./lib/');
-},{"./lib/":104}],104:[function(require,module,exports){
+},{"./lib/":110}],110:[function(require,module,exports){
/**
* Module dependencies.
@@ -30911,7 +30904,7 @@ exports.connect = lookup;
exports.Manager = require('./manager');
exports.Socket = require('./socket');
-},{"./manager":105,"./socket":107,"./url":108,"debug":112,"socket.io-parser":149}],105:[function(require,module,exports){
+},{"./manager":111,"./socket":113,"./url":114,"debug":118,"socket.io-parser":154}],111:[function(require,module,exports){
/**
* Module dependencies.
@@ -31416,7 +31409,7 @@ Manager.prototype.onreconnect = function(){
this.emitAll('reconnect', attempt);
};
-},{"./on":106,"./socket":107,"./url":108,"backo2":109,"component-bind":110,"component-emitter":111,"debug":112,"engine.io-client":113,"indexof":145,"object-component":146,"socket.io-parser":149}],106:[function(require,module,exports){
+},{"./on":112,"./socket":113,"./url":114,"backo2":115,"component-bind":116,"component-emitter":117,"debug":118,"engine.io-client":119,"indexof":150,"object-component":151,"socket.io-parser":154}],112:[function(require,module,exports){
/**
* Module exports.
@@ -31442,7 +31435,7 @@ function on(obj, ev, fn) {
};
}
-},{}],107:[function(require,module,exports){
+},{}],113:[function(require,module,exports){
/**
* Module dependencies.
@@ -31829,7 +31822,7 @@ Socket.prototype.disconnect = function(){
return this;
};
-},{"./on":106,"component-bind":110,"component-emitter":111,"debug":112,"has-binary":143,"socket.io-parser":149,"to-array":155}],108:[function(require,module,exports){
+},{"./on":112,"component-bind":116,"component-emitter":117,"debug":118,"has-binary":148,"socket.io-parser":154,"to-array":158}],114:[function(require,module,exports){
(function (global){
/**
@@ -31906,7 +31899,7 @@ function url(uri, loc){
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"debug":112,"parseuri":147}],109:[function(require,module,exports){
+},{"debug":118,"parseuri":152}],115:[function(require,module,exports){
/**
* Expose `Backoff`.
@@ -31993,7 +31986,7 @@ Backoff.prototype.setJitter = function(jitter){
};
-},{}],110:[function(require,module,exports){
+},{}],116:[function(require,module,exports){
/**
* Slice reference.
*/
@@ -32018,7 +32011,7 @@ module.exports = function(obj, fn){
}
};
-},{}],111:[function(require,module,exports){
+},{}],117:[function(require,module,exports){
/**
* Expose `Emitter`.
@@ -32184,7 +32177,7 @@ Emitter.prototype.hasListeners = function(event){
return !! this.listeners(event).length;
};
-},{}],112:[function(require,module,exports){
+},{}],118:[function(require,module,exports){
/**
* Expose `debug()` as the module.
@@ -32323,11 +32316,11 @@ try {
if (window.localStorage) debug.enable(localStorage.debug);
} catch(e){}
-},{}],113:[function(require,module,exports){
+},{}],119:[function(require,module,exports){
module.exports = require('./lib/');
-},{"./lib/":114}],114:[function(require,module,exports){
+},{"./lib/":120}],120:[function(require,module,exports){
module.exports = require('./socket');
@@ -32339,7 +32332,7 @@ module.exports = require('./socket');
*/
module.exports.parser = require('engine.io-parser');
-},{"./socket":115,"engine.io-parser":128}],115:[function(require,module,exports){
+},{"./socket":121,"engine.io-parser":133}],121:[function(require,module,exports){
(function (global){
/**
* Module dependencies.
@@ -33048,7 +33041,7 @@ Socket.prototype.filterUpgrades = function (upgrades) {
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./transport":116,"./transports":117,"component-emitter":123,"debug":125,"engine.io-parser":128,"indexof":145,"parsejson":139,"parseqs":140,"parseuri":141}],116:[function(require,module,exports){
+},{"./transport":122,"./transports":123,"component-emitter":117,"debug":130,"engine.io-parser":133,"indexof":150,"parsejson":144,"parseqs":145,"parseuri":146}],122:[function(require,module,exports){
/**
* Module dependencies.
*/
@@ -33209,7 +33202,7 @@ Transport.prototype.onClose = function () {
this.emit('close');
};
-},{"component-emitter":123,"engine.io-parser":128}],117:[function(require,module,exports){
+},{"component-emitter":117,"engine.io-parser":133}],123:[function(require,module,exports){
(function (global){
/**
* Module dependencies
@@ -33266,7 +33259,7 @@ function polling(opts){
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./polling-jsonp":118,"./polling-xhr":119,"./websocket":121,"xmlhttprequest":122}],118:[function(require,module,exports){
+},{"./polling-jsonp":124,"./polling-xhr":125,"./websocket":127,"xmlhttprequest":128}],124:[function(require,module,exports){
(function (global){
/**
@@ -33503,7 +33496,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) {
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./polling":120,"component-inherit":124}],119:[function(require,module,exports){
+},{"./polling":126,"component-inherit":129}],125:[function(require,module,exports){
(function (global){
/**
* Module requirements.
@@ -33891,7 +33884,7 @@ function unloadHandler() {
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./polling":120,"component-emitter":123,"component-inherit":124,"debug":125,"xmlhttprequest":122}],120:[function(require,module,exports){
+},{"./polling":126,"component-emitter":117,"component-inherit":129,"debug":130,"xmlhttprequest":128}],126:[function(require,module,exports){
/**
* Module dependencies.
*/
@@ -34138,7 +34131,7 @@ Polling.prototype.uri = function(){
return schema + '://' + this.hostname + port + this.path + query;
};
-},{"../transport":116,"component-inherit":124,"debug":125,"engine.io-parser":128,"parseqs":140,"xmlhttprequest":122}],121:[function(require,module,exports){
+},{"../transport":122,"component-inherit":129,"debug":130,"engine.io-parser":133,"parseqs":145,"xmlhttprequest":128}],127:[function(require,module,exports){
/**
* Module dependencies.
*/
@@ -34378,7 +34371,7 @@ WS.prototype.check = function(){
return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name);
};
-},{"../transport":116,"component-inherit":124,"debug":125,"engine.io-parser":128,"parseqs":140,"ws":142}],122:[function(require,module,exports){
+},{"../transport":122,"component-inherit":129,"debug":130,"engine.io-parser":133,"parseqs":145,"ws":147}],128:[function(require,module,exports){
// browser shim for xmlhttprequest module
var hasCORS = require('has-cors');
@@ -34416,9 +34409,7 @@ module.exports = function(opts) {
}
}
-},{"has-cors":137}],123:[function(require,module,exports){
-arguments[4][111][0].apply(exports,arguments)
-},{"dup":111}],124:[function(require,module,exports){
+},{"has-cors":142}],129:[function(require,module,exports){
module.exports = function(a, b){
var fn = function(){};
@@ -34426,7 +34417,7 @@ module.exports = function(a, b){
a.prototype = new fn;
a.prototype.constructor = a;
};
-},{}],125:[function(require,module,exports){
+},{}],130:[function(require,module,exports){
/**
* This is the web browser implementation of `debug()`.
@@ -34575,7 +34566,7 @@ function load() {
exports.enable(load());
-},{"./debug":126}],126:[function(require,module,exports){
+},{"./debug":131}],131:[function(require,module,exports){
/**
* This is the common logic for both the Node.js and web browser
@@ -34774,7 +34765,7 @@ function coerce(val) {
return val;
}
-},{"ms":127}],127:[function(require,module,exports){
+},{"ms":132}],132:[function(require,module,exports){
/**
* Helpers.
*/
@@ -34887,7 +34878,7 @@ function plural(ms, n, name) {
return Math.ceil(ms / n) + ' ' + name + 's';
}
-},{}],128:[function(require,module,exports){
+},{}],133:[function(require,module,exports){
(function (global){
/**
* Module dependencies.
@@ -35485,7 +35476,7 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) {
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./keys":129,"after":130,"arraybuffer.slice":131,"base64-arraybuffer":132,"blob":133,"has-binary":134,"utf8":136}],129:[function(require,module,exports){
+},{"./keys":134,"after":135,"arraybuffer.slice":136,"base64-arraybuffer":137,"blob":138,"has-binary":139,"utf8":141}],134:[function(require,module,exports){
/**
* Gets the keys for an object.
@@ -35506,7 +35497,7 @@ module.exports = Object.keys || function keys (obj){
return arr;
};
-},{}],130:[function(require,module,exports){
+},{}],135:[function(require,module,exports){
module.exports = after
function after(count, callback, err_cb) {
@@ -35536,7 +35527,7 @@ function after(count, callback, err_cb) {
function noop() {}
-},{}],131:[function(require,module,exports){
+},{}],136:[function(require,module,exports){
/**
* An abstraction for slicing an arraybuffer even when
* ArrayBuffer.prototype.slice is not supported
@@ -35567,7 +35558,7 @@ module.exports = function(arraybuffer, start, end) {
return result.buffer;
};
-},{}],132:[function(require,module,exports){
+},{}],137:[function(require,module,exports){
/*
* base64-arraybuffer
* https://github.com/niklasvh/base64-arraybuffer
@@ -35628,7 +35619,7 @@ module.exports = function(arraybuffer, start, end) {
};
})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
-},{}],133:[function(require,module,exports){
+},{}],138:[function(require,module,exports){
(function (global){
/**
* Create a blob builder even when vendor prefixes exist
@@ -35681,7 +35672,7 @@ module.exports = (function() {
})();
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],134:[function(require,module,exports){
+},{}],139:[function(require,module,exports){
(function (global){
/*
@@ -35743,12 +35734,12 @@ function hasBinary(data) {
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"isarray":135}],135:[function(require,module,exports){
+},{"isarray":140}],140:[function(require,module,exports){
module.exports = Array.isArray || function (arr) {
return Object.prototype.toString.call(arr) == '[object Array]';
};
-},{}],136:[function(require,module,exports){
+},{}],141:[function(require,module,exports){
(function (global){
/*! http://mths.be/utf8js v2.0.0 by @mathias */
;(function(root) {
@@ -35991,7 +35982,7 @@ module.exports = Array.isArray || function (arr) {
}(this));
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],137:[function(require,module,exports){
+},{}],142:[function(require,module,exports){
/**
* Module dependencies.
@@ -36016,7 +36007,7 @@ try {
module.exports = false;
}
-},{"global":138}],138:[function(require,module,exports){
+},{"global":143}],143:[function(require,module,exports){
/**
* Returns `this`. Execute this without a "context" (i.e. without it being
@@ -36026,7 +36017,7 @@ try {
module.exports = (function () { return this; })();
-},{}],139:[function(require,module,exports){
+},{}],144:[function(require,module,exports){
(function (global){
/**
* JSON parse.
@@ -36061,7 +36052,7 @@ module.exports = function parsejson(data) {
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],140:[function(require,module,exports){
+},{}],145:[function(require,module,exports){
/**
* Compiles a querystring
* Returns string representation of the object
@@ -36100,7 +36091,7 @@ exports.decode = function(qs){
return qry;
};
-},{}],141:[function(require,module,exports){
+},{}],146:[function(require,module,exports){
/**
* Parses an URI
*
@@ -36141,7 +36132,7 @@ module.exports = function parseuri(str) {
return uri;
};
-},{}],142:[function(require,module,exports){
+},{}],147:[function(require,module,exports){
/**
* Module dependencies.
@@ -36186,7 +36177,7 @@ function ws(uri, protocols, opts) {
if (WebSocket) ws.prototype = WebSocket.prototype;
-},{}],143:[function(require,module,exports){
+},{}],148:[function(require,module,exports){
(function (global){
/*
@@ -36248,9 +36239,9 @@ function hasBinary(data) {
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"isarray":144}],144:[function(require,module,exports){
-arguments[4][135][0].apply(exports,arguments)
-},{"dup":135}],145:[function(require,module,exports){
+},{"isarray":149}],149:[function(require,module,exports){
+arguments[4][140][0].apply(exports,arguments)
+},{"dup":140}],150:[function(require,module,exports){
var indexOf = [].indexOf;
@@ -36261,7 +36252,7 @@ module.exports = function(arr, obj){
}
return -1;
};
-},{}],146:[function(require,module,exports){
+},{}],151:[function(require,module,exports){
/**
* HOP ref.
@@ -36346,7 +36337,7 @@ exports.length = function(obj){
exports.isEmpty = function(obj){
return 0 == exports.length(obj);
};
-},{}],147:[function(require,module,exports){
+},{}],152:[function(require,module,exports){
/**
* Parses an URI
*
@@ -36373,7 +36364,7 @@ module.exports = function parseuri(str) {
return uri;
};
-},{}],148:[function(require,module,exports){
+},{}],153:[function(require,module,exports){
(function (global){
/*global Blob,File*/
@@ -36518,7 +36509,7 @@ exports.removeBlobs = function(data, callback) {
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./is-buffer":150,"isarray":153}],149:[function(require,module,exports){
+},{"./is-buffer":155,"isarray":156}],154:[function(require,module,exports){
/**
* Module dependencies.
@@ -36920,7 +36911,7 @@ function error(data){
};
}
-},{"./binary":148,"./is-buffer":150,"component-emitter":151,"debug":152,"isarray":153,"json3":154}],150:[function(require,module,exports){
+},{"./binary":153,"./is-buffer":155,"component-emitter":117,"debug":118,"isarray":156,"json3":157}],155:[function(require,module,exports){
(function (global){
module.exports = isBuf;
@@ -36937,13 +36928,9 @@ function isBuf(obj) {
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],151:[function(require,module,exports){
-arguments[4][111][0].apply(exports,arguments)
-},{"dup":111}],152:[function(require,module,exports){
-arguments[4][112][0].apply(exports,arguments)
-},{"dup":112}],153:[function(require,module,exports){
-arguments[4][135][0].apply(exports,arguments)
-},{"dup":135}],154:[function(require,module,exports){
+},{}],156:[function(require,module,exports){
+arguments[4][140][0].apply(exports,arguments)
+},{"dup":140}],157:[function(require,module,exports){
/*! JSON v3.2.6 | http://bestiejs.github.io/json3 | Copyright 2012-2013, Kit Cambridge | http://kit.mit-license.org */
;(function (window) {
// Convenience aliases.
@@ -37806,7 +37793,7 @@ arguments[4][135][0].apply(exports,arguments)
}
}(this));
-},{}],155:[function(require,module,exports){
+},{}],158:[function(require,module,exports){
module.exports = toArray
function toArray(list, index) {
@@ -37821,250 +37808,645 @@ function toArray(list, index) {
return array
}
-},{}],156:[function(require,module,exports){
-var MediaStreamType = {
- VIDEO_TYPE: "Video",
-
- AUDIO_TYPE: "Audio"
-};
-module.exports = MediaStreamType;
-},{}],157:[function(require,module,exports){
-var RTCEvents = {
- RTC_READY: "rtc.ready",
- DATA_CHANNEL_OPEN: "rtc.data_channel_open",
- LASTN_CHANGED: "rtc.lastn_changed",
- DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
- LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
- AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
- AUDIO_MUTE: "rtc.audio_mute",
- VIDEO_MUTE: "rtc.video_mute"
-};
-
-module.exports = RTCEvents;
-},{}],158:[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;
},{}],159:[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"
-};
-
-module.exports = StreamEventTypes;
-},{}],160:[function(require,module,exports){
-var UIEvents = {
- NICKNAME_CHANGED: "UI.nickname_changed",
- SELECTED_ENDPOINT: "UI.selected_endpoint",
- PINNED_ENDPOINT: "UI.pinned_endpoint",
- LARGEVIDEO_INIT: "UI.largevideo_init"
-};
-module.exports = UIEvents;
-},{}],161:[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 MediaStreamType = {
+ VIDEO_TYPE: "Video",
+ AUDIO_TYPE: "Audio"
+};
+module.exports = MediaStreamType;
+},{}],160:[function(require,module,exports){
+var RTCEvents = {
+ RTC_READY: "rtc.ready",
+ DATA_CHANNEL_OPEN: "rtc.data_channel_open",
+ LASTN_CHANGED: "rtc.lastn_changed",
+ DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
+ LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
+ AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
+ AUDIO_MUTE: "rtc.audio_mute",
+ VIDEO_MUTE: "rtc.video_mute"
+};
+
+module.exports = RTCEvents;
+},{}],161:[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;
},{}],162:[function(require,module,exports){
-var CQEvents = {
- LOCALSTATS_UPDATED: "cq.localstats_updated",
- REMOTESTATS_UPDATED: "cq.remotestats_updated",
- STOP: "cq.stop"
-};
-
-module.exports = CQEvents;
+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"
+};
+
+module.exports = StreamEventTypes;
},{}],163:[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 UIEvents = {
+ NICKNAME_CHANGED: "UI.nickname_changed",
+ SELECTED_ENDPOINT: "UI.selected_endpoint",
+ PINNED_ENDPOINT: "UI.pinned_endpoint",
+ LARGEVIDEO_INIT: "UI.largevideo_init"
+};
+module.exports = UIEvents;
},{}],164:[function(require,module,exports){
-var Events = {
- DTMF_SUPPORT_CHANGED: "members.dtmf_support_changed"
-};
-
-module.exports = Events;
+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;
},{}],165:[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",
- FR: "fr"
-}
+var CQEvents = {
+ LOCALSTATS_UPDATED: "cq.localstats_updated",
+ REMOTESTATS_UPDATED: "cq.remotestats_updated",
+ STOP: "cq.stop"
+};
+
+module.exports = CQEvents;
},{}],166:[function(require,module,exports){
-var XMPPEvents = {
- // Designates an event indicating that the connection to the XMPP server
- // failed.
- CONNECTION_FAILED: "xmpp.connection.failed",
- // Designates an event indicating that the media (ICE) connection was
- // interrupted. This should go to the RTC module.
- CONNECTION_INTERRUPTED: "xmpp.connection.interrupted",
- // Designates an event indicating that the media (ICE) connection was
- // restored. This should go to the RTC module.
- CONNECTION_RESTORED: "xmpp.connection.restored",
- // Designates an event indicating that an offer (e.g. Jingle
- // session-initiate) was received.
- CALL_INCOMING: "xmpp.callincoming.jingle",
- // Designates an event indicating that we were kicked from the XMPP MUC.
- KICKED: "xmpp.kicked",
- // Designates an event indicating that the userID for a specific JID has
- // changed.
- USER_ID_CHANGED: "xmpp.user_id_changed",
- // Designates an event indicating that we have joined the XMPP MUC.
- MUC_JOINED: "xmpp.muc_joined",
- // Designates an event indicating that a participant joined the XMPP MUC.
- MUC_MEMBER_JOINED: "xmpp.muc_member_joined",
- // Designates an event indicating that a participant left the XMPP MUC.
- MUC_MEMBER_LEFT: "xmpp.muc_member_left",
- // Designates an event indicating that the MUC role of a participant has
- // changed.
- MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
- // Designates an event indicating that the XMPP MUC was destroyed.
- MUC_DESTROYED: "xmpp.muc_destroyed",
- // Designates an event indicating that the display name of a participant
- // has changed.
- DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
- // Designates an event indicating that we received statistics from a
- // participant in the MUC.
- REMOTE_STATS: "xmpp.remote_stats",
- // Designates an event indicating that our role in the XMPP MUC has changed.
- LOCAL_ROLE_CHANGED: "xmpp.localrole_changed",
- // Designates an event indicating that the subject of the XMPP MUC has
- // changed.
- SUBJECT_CHANGED: "xmpp.subject_changed",
- // Designates an event indicating that an XMPP message in the MUC was
- // received.
- MESSAGE_RECEIVED: "xmpp.message_received",
- // Designates an event indicating that we sent an XMPP message to the MUC.
- SENDING_CHAT_MESSAGE: "xmpp.sending_chat_message",
- // Designates an event indicating that the video type (e.g. 'camera' or
- // 'screen') for a participant has changed.
- PARTICIPANT_VIDEO_TYPE_CHANGED: "xmpp.video_type",
- // Designates an event indicating that a participant in the XMPP MUC has
- // advertised that they have audio muted (or unmuted).
- PARTICIPANT_AUDIO_MUTED: "xmpp.audio_muted",
- // Designates an event indicating that a participant in the XMPP MUC has
- // advertised that they have video muted (or unmuted).
- PARTICIPANT_VIDEO_MUTED: "xmpp.video_muted",
- // Designates an event indicating that the focus has asked us to mute our
- // audio.
- AUDIO_MUTED_BY_FOCUS: "xmpp.audio_muted_by_focus",
- // Designates an event indicating that a moderator in the room changed the
- // "start muted" settings for the conference.
- START_MUTED_SETTING_CHANGED: "xmpp.start_muted_setting_changed",
- // Designates an event indicating that we should join the conference with
- // audio and/or video muted.
- START_MUTED_FROM_FOCUS: "xmpp.start_muted_from_focus",
-
-
- PEERCONNECTION_READY: "xmpp.peerconnection_ready",
- CONFERENCE_SETUP_FAILED: "xmpp.conference_setup_failed",
- PASSWORD_REQUIRED: "xmpp.password_required",
- AUTHENTICATION_REQUIRED: "xmpp.authentication_required",
- CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
- ETHERPAD: "xmpp.etherpad",
- DEVICE_AVAILABLE: "xmpp.device_available",
- BRIDGE_DOWN: "xmpp.bridge_down",
- PRESENCE_STATUS: "xmpp.presence_status",
- RESERVATION_ERROR: "xmpp.room_reservation_error",
- DISPOSE_CONFERENCE: "xmpp.dispose_conference",
- GRACEFUL_SHUTDOWN: "xmpp.graceful_shutdown",
- // TODO: only used in a hack, should probably be removed.
- SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
- // TODO: only used in a hack, should probably be removed.
- SET_REMOTE_DESCRIPTION_ERROR: 'xmpp.set_remote_description_error',
- // TODO: only used in a hack, should probably be removed.
- CREATE_ANSWER_ERROR: 'xmpp.create_answer_error',
- JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error',
- PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
- FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
- ROOM_JOIN_ERROR: 'xmpp.room_join_error',
- ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
- // xmpp is connected and obtained user media
- READY_TO_JOIN: 'xmpp.ready_to_join'
-};
+var DesktopSharingEventTypes = {
+ INIT: "ds.init",
+
+ SWITCHING_DONE: "ds.switching_done",
+
+ NEW_STREAM_CREATED: "ds.new_stream_created"
+};
+
+module.exports = DesktopSharingEventTypes;
+},{}],167:[function(require,module,exports){
+var Events = {
+ DTMF_SUPPORT_CHANGED: "members.dtmf_support_changed"
+};
+
+module.exports = Events;
+
+},{}],168:[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",
+ FR: "fr"
+}
+},{}],169:[function(require,module,exports){
+var XMPPEvents = {
+ // Designates an event indicating that the connection to the XMPP server
+ // failed.
+ CONNECTION_FAILED: "xmpp.connection.failed",
+ // Designates an event indicating that the media (ICE) connection was
+ // interrupted. This should go to the RTC module.
+ CONNECTION_INTERRUPTED: "xmpp.connection.interrupted",
+ // Designates an event indicating that the media (ICE) connection was
+ // restored. This should go to the RTC module.
+ CONNECTION_RESTORED: "xmpp.connection.restored",
+ // Designates an event indicating that an offer (e.g. Jingle
+ // session-initiate) was received.
+ CALL_INCOMING: "xmpp.callincoming.jingle",
+ // Designates an event indicating that we were kicked from the XMPP MUC.
+ KICKED: "xmpp.kicked",
+ // Designates an event indicating that the userID for a specific JID has
+ // changed.
+ USER_ID_CHANGED: "xmpp.user_id_changed",
+ // Designates an event indicating that we have joined the XMPP MUC.
+ MUC_JOINED: "xmpp.muc_joined",
+ // Designates an event indicating that a participant joined the XMPP MUC.
+ MUC_MEMBER_JOINED: "xmpp.muc_member_joined",
+ // Designates an event indicating that a participant left the XMPP MUC.
+ MUC_MEMBER_LEFT: "xmpp.muc_member_left",
+ // Designates an event indicating that the MUC role of a participant has
+ // changed.
+ MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
+ // Designates an event indicating that the XMPP MUC was destroyed.
+ MUC_DESTROYED: "xmpp.muc_destroyed",
+ // Designates an event indicating that the display name of a participant
+ // has changed.
+ DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
+ // Designates an event indicating that we received statistics from a
+ // participant in the MUC.
+ REMOTE_STATS: "xmpp.remote_stats",
+ // Designates an event indicating that our role in the XMPP MUC has changed.
+ LOCAL_ROLE_CHANGED: "xmpp.localrole_changed",
+ // Designates an event indicating that the subject of the XMPP MUC has
+ // changed.
+ SUBJECT_CHANGED: "xmpp.subject_changed",
+ // Designates an event indicating that an XMPP message in the MUC was
+ // received.
+ MESSAGE_RECEIVED: "xmpp.message_received",
+ // Designates an event indicating that we sent an XMPP message to the MUC.
+ SENDING_CHAT_MESSAGE: "xmpp.sending_chat_message",
+ // Designates an event indicating that the video type (e.g. 'camera' or
+ // 'screen') for a participant has changed.
+ PARTICIPANT_VIDEO_TYPE_CHANGED: "xmpp.video_type",
+ // Designates an event indicating that a participant in the XMPP MUC has
+ // advertised that they have audio muted (or unmuted).
+ PARTICIPANT_AUDIO_MUTED: "xmpp.audio_muted",
+ // Designates an event indicating that a participant in the XMPP MUC has
+ // advertised that they have video muted (or unmuted).
+ PARTICIPANT_VIDEO_MUTED: "xmpp.video_muted",
+ // Designates an event indicating that the focus has asked us to mute our
+ // audio.
+ AUDIO_MUTED_BY_FOCUS: "xmpp.audio_muted_by_focus",
+ // Designates an event indicating that a moderator in the room changed the
+ // "start muted" settings for the conference.
+ START_MUTED_SETTING_CHANGED: "xmpp.start_muted_setting_changed",
+ // Designates an event indicating that we should join the conference with
+ // audio and/or video muted.
+ START_MUTED_FROM_FOCUS: "xmpp.start_muted_from_focus",
+
+
+ PEERCONNECTION_READY: "xmpp.peerconnection_ready",
+ CONFERENCE_SETUP_FAILED: "xmpp.conference_setup_failed",
+ PASSWORD_REQUIRED: "xmpp.password_required",
+ AUTHENTICATION_REQUIRED: "xmpp.authentication_required",
+ CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
+ ETHERPAD: "xmpp.etherpad",
+ DEVICE_AVAILABLE: "xmpp.device_available",
+ BRIDGE_DOWN: "xmpp.bridge_down",
+ PRESENCE_STATUS: "xmpp.presence_status",
+ RESERVATION_ERROR: "xmpp.room_reservation_error",
+ DISPOSE_CONFERENCE: "xmpp.dispose_conference",
+ GRACEFUL_SHUTDOWN: "xmpp.graceful_shutdown",
+ // TODO: only used in a hack, should probably be removed.
+ SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
+ // TODO: only used in a hack, should probably be removed.
+ SET_REMOTE_DESCRIPTION_ERROR: 'xmpp.set_remote_description_error',
+ // TODO: only used in a hack, should probably be removed.
+ CREATE_ANSWER_ERROR: 'xmpp.create_answer_error',
+ JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error',
+ PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
+ FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
+ ROOM_JOIN_ERROR: 'xmpp.room_join_error',
+ ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
+ // xmpp is connected and obtained user media
+ READY_TO_JOIN: 'xmpp.ready_to_join'
+};
module.exports = XMPPEvents;
-},{}]},{},[3])(3)
+},{}],170:[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;
+}
+
+},{}],171:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+ draining = false;
+ if (currentQueue.length) {
+ queue = currentQueue.concat(queue);
+ } else {
+ queueIndex = -1;
+ }
+ if (queue.length) {
+ drainQueue();
+ }
+}
+
+function drainQueue() {
+ if (draining) {
+ return;
+ }
+ var timeout = setTimeout(cleanUpNextTick);
+ draining = true;
+
+ var len = queue.length;
+ while(len) {
+ currentQueue = queue;
+ queue = [];
+ while (++queueIndex < len) {
+ currentQueue[queueIndex].run();
+ }
+ queueIndex = -1;
+ len = queue.length;
+ }
+ currentQueue = null;
+ draining = false;
+ clearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+ var args = new Array(arguments.length - 1);
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ }
+ queue.push(new Item(fun, args));
+ if (queue.length === 1 && !draining) {
+ setTimeout(drainQueue, 0);
+ }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+ this.fun = fun;
+ this.array = array;
+}
+Item.prototype.run = function () {
+ this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+ throw new Error('process.binding is not supported');
+};
+
+// TODO(shtylman)
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+ throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+},{}]},{},[1])(1)
});
\ No newline at end of file
diff --git a/modules/statistics/RTPStatsCollector.js b/modules/statistics/RTPStatsCollector.js
index 24b426ac2..01db987b0 100644
--- a/modules/statistics/RTPStatsCollector.js
+++ b/modules/statistics/RTPStatsCollector.js
@@ -4,7 +4,7 @@ var RTCBrowserType = require("../RTC/RTCBrowserType");
/* Whether we support the browser we are running into for logging statistics */
var browserSupported = RTCBrowserType.isChrome() ||
- RTCBrowserType.isOpera();
+ RTCBrowserType.isOpera() || RTCBrowserType.isFirefox();
/**
* Calculates packet lost percent using the number of lost packets and the
* number of all packet.
@@ -296,7 +296,8 @@ StatsCollector.prototype.start = function ()
);
}
- if (config.logStats && browserSupported) {
+ // Logging statistics does not support firefox
+ if (config.logStats && (browserSupported && !RTCBrowserType.isFirefox())) {
this.gatherStatsIntervalId = setInterval(
function () {
self.peerconnection.getStats(