diff --git a/.gitignore b/.gitignore index 92772c563..c0dc2e1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .idea/ *.iml .*.tmp +deploy-local.sh diff --git a/Makefile b/Makefile index e37134814..1dca402b0 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,10 @@ app: $(NPM) update && $(BROWSERIFY) $(FLAGS) app.js -s APP -o $(OUTPUT_DIR)/app.bundle.js clean: - @rm -f $(OUTPUT_DIR)/*.bundle.js + rm -f $(OUTPUT_DIR)/*.bundle.js deploy: - @mkdir -p $(DEPLOY_DIR) && cp $(OUTPUT_DIR)/*.bundle.js $(DEPLOY_DIR) && ./bump-js-versions.sh + mkdir -p $(DEPLOY_DIR) && \ + cp $(OUTPUT_DIR)/*.bundle.js $(DEPLOY_DIR) && \ + ./bump-js-versions.sh && \ + ([ ! -x deploy-local.sh ] || ./deploy-local.sh) diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 871b36462..c9ca1fc87 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -446,6 +446,11 @@ background-position: center; } +.videoMessageFilter { + -webkit-filter: grayscale(.5) opacity(0.8); + filter: grayscale(.5) opacity(0.8); +} + .videoProblemFilter { -webkit-filter: blur(10px) grayscale(.5) opacity(0.8); filter: blur(10px) grayscale(.5) opacity(0.8); diff --git a/doc/api.md b/doc/api.md index d52fc0e06..139729904 100644 --- a/doc/api.md +++ b/doc/api.md @@ -29,6 +29,11 @@ constructor. ``` If you don't specify room the user will enter in new conference with random room name. +You can enable the "film strip only" mode(only the small videos are visible) by setting 6th parameter to ```true```: +```javascript + var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, true); +``` + Controlling embedded Jitsi Meet Conference ========= diff --git a/external_api.js b/external_api.js index 91996e08e..5d23daeb7 100644 --- a/external_api.js +++ b/external_api.js @@ -23,9 +23,12 @@ var JitsiMeetExternalAPI = (function() * @param width width of the iframe * @param height height of the iframe * @param parent_node the node that will contain the iframe + * @param filmStripOnly if the value is true only the small videos will be + * visible. * @constructor */ - function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode) { + function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, + filmStripOnly) { if(!width || width < MIN_WIDTH) width = MIN_WIDTH; if(!height || height < MIN_HEIGHT) @@ -49,6 +52,9 @@ var JitsiMeetExternalAPI = (function() if(room_name) this.url += room_name; this.url += "#external=true"; + if(filmStripOnly) + this.url += "&interfaceConfig.filmStripOnly=true"; + JitsiMeetExternalAPI.id++; this.frame = document.createElement("iframe"); diff --git a/index.html b/index.html index 981c198e7..ba7dab16b 100644 --- a/index.html +++ b/index.html @@ -22,7 +22,7 @@ - + diff --git a/interface_config.js b/interface_config.js index e664812ee..0317683e8 100644 --- a/interface_config.js +++ b/interface_config.js @@ -15,5 +15,9 @@ var interfaceConfig = { GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true, APP_NAME: "Jitsi Meet", INVITATION_POWERED_BY: true, - ACTIVE_SPEAKER_AVATAR_SIZE: 100 + ACTIVE_SPEAKER_AVATAR_SIZE: 100, + /** + * Whether to only show the filmstrip (and hide the toolbar). + */ + filmStripOnly: false }; diff --git a/lang/main.json b/lang/main.json index acab49f08..29df8de17 100644 --- a/lang/main.json +++ b/lang/main.json @@ -240,5 +240,11 @@ "FETCH_SESSION_ID": "Obtaining session-id...", "GOT_SESSION_ID": "Obtaining session-id... Done", "GET_SESSION_ID_ERROR": "Get session-id error: " + }, + "recording": + { + "toaster": "Currently recording!", + "pending": "Your recording will start as soon as another participant joins", + "on": "Recording has been started" } } diff --git a/libs/app.bundle.js b/libs/app.bundle.js index 4956f5dbd..a1634af23 100644 --- a/libs/app.bundle.js +++ b/libs/app.bundle.js @@ -450,7 +450,7 @@ $(window).bind('beforeunload', function () { module.exports = APP; -},{"./modules/API/API":4,"./modules/DTMF/DTMF":5,"./modules/RTC/RTC":9,"./modules/UI/UI":13,"./modules/URLProcessor/URLProcessor":44,"./modules/connectionquality/connectionquality":45,"./modules/desktopsharing/desktopsharing":46,"./modules/keyboardshortcut/keyboardshortcut":47,"./modules/members/MemberList":48,"./modules/settings/Settings":49,"./modules/statistics/statistics":53,"./modules/translation/translation":54,"./modules/xmpp/xmpp":69}],4:[function(require,module,exports){ +},{"./modules/API/API":4,"./modules/DTMF/DTMF":5,"./modules/RTC/RTC":9,"./modules/UI/UI":13,"./modules/URLProcessor/URLProcessor":44,"./modules/connectionquality/connectionquality":45,"./modules/desktopsharing/desktopsharing":46,"./modules/keyboardshortcut/keyboardshortcut":47,"./modules/members/MemberList":48,"./modules/settings/Settings":49,"./modules/statistics/statistics":53,"./modules/translation/translation":54,"./modules/xmpp/xmpp":70}],4:[function(require,module,exports){ /* global APP */ /** * Implements API class that communicates with external api class @@ -670,7 +670,7 @@ var API = { }; module.exports = API; -},{"../../service/xmpp/XMPPEvents":119}],5:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120}],5:[function(require,module,exports){ /* global APP */ /** @@ -912,10 +912,28 @@ function onPinnedEndpointChanged(userResource) { module.exports = DataChannels; -},{"../../service/RTC/RTCEvents":110}],7:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111}],7:[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; @@ -936,9 +954,12 @@ function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) { }; } - this.stream.onended = function() { + this.stream.onended = function () { self.streamEnded(); }; + if (RTCBrowserType.isFirefox()) { + implementOnEndedHandling(this.stream); + } } LocalStream.prototype.streamEnded = function () { @@ -960,9 +981,11 @@ LocalStream.prototype.setMute = function (mute) var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE; if ((window.location.protocol != "https:" && this.isGUMStream) || - (isAudio && this.isGUMStream) || this.videoType === "screen") { - var tracks = this.getTracks(); + (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; } @@ -1015,7 +1038,7 @@ LocalStream.prototype.getId = function () { module.exports = LocalStream; -},{"../../service/RTC/RTCEvents":110,"../../service/RTC/StreamEventTypes.js":112}],8:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111,"../../service/RTC/StreamEventTypes.js":113,"./RTCBrowserType":10}],8:[function(require,module,exports){ var MediaStreamType = require("../../service/RTC/MediaStreamTypes"); /** @@ -1064,7 +1087,7 @@ MediaStream.prototype.setMute = function (value) { module.exports = MediaStream; -},{"../../service/RTC/MediaStreamTypes":109}],9:[function(require,module,exports){ +},{"../../service/RTC/MediaStreamTypes":110}],9:[function(require,module,exports){ /* global APP */ var EventEmitter = require("events"); var RTCBrowserType = require("./RTCBrowserType"); @@ -1341,7 +1364,7 @@ var RTC = { module.exports = RTC; -},{"../../service/RTC/MediaStreamTypes":109,"../../service/RTC/RTCEvents.js":110,"../../service/RTC/StreamEventTypes.js":112,"../../service/UI/UIEvents":113,"../../service/desktopsharing/DesktopSharingEventTypes":116,"../../service/xmpp/XMPPEvents":119,"./DataChannels":6,"./LocalStream.js":7,"./MediaStream.js":8,"./RTCBrowserType":10,"./RTCUtils.js":11,"events":1}],10:[function(require,module,exports){ +},{"../../service/RTC/MediaStreamTypes":110,"../../service/RTC/RTCEvents.js":111,"../../service/RTC/StreamEventTypes.js":113,"../../service/UI/UIEvents":114,"../../service/desktopsharing/DesktopSharingEventTypes":117,"../../service/xmpp/XMPPEvents":120,"./DataChannels":6,"./LocalStream.js":7,"./MediaStream.js":8,"./RTCBrowserType":10,"./RTCUtils.js":11,"events":1}],10:[function(require,module,exports){ var currentBrowser; @@ -1529,21 +1552,17 @@ function getPreviousResolution(resolution) { } function setResolutionConstraints(constraints, resolution, isAndroid) { - if (resolution && !constraints.video || isAndroid) { - // same behaviour as true - constraints.video = { mandatory: {}, optional: [] }; - } - if(Resolutions[resolution]) { + if (Resolutions[resolution]) { constraints.video.mandatory.minWidth = Resolutions[resolution].width; constraints.video.mandatory.minHeight = Resolutions[resolution].height; } - else { - if (isAndroid) { - constraints.video.mandatory.minWidth = 320; - constraints.video.mandatory.minHeight = 240; - constraints.video.mandatory.maxFrameRate = 15; - } + 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) @@ -1561,10 +1580,28 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid if (um.indexOf('video') >= 0) { // same behaviour as true constraints.video = { mandatory: {}, optional: [] }; + + constraints.video.optional.push({ googLeakyBucket: true }); + + setResolutionConstraints(constraints, resolution, isAndroid); } if (um.indexOf('audio') >= 0) { - // same behaviour as true - constraints.audio = { mandatory: {}, optional: []}; + 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()) { @@ -1606,30 +1643,6 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid }; } - if (constraints.audio) { - // 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} - ); - } - if (constraints.video) { - if (um.indexOf('video') >= 0) { - constraints.video.optional.push( - {googLeakyBucket: true} - ); - } - } - - if (um.indexOf('video') >= 0) { - setResolutionConstraints(constraints, resolution, isAndroid); - } - if (bandwidth) { if (!constraints.video) { //same behaviour as true @@ -2068,7 +2081,7 @@ RTCUtils.prototype.createStream = function(stream, isVideo) { module.exports = RTCUtils; -},{"../../service/RTC/Resolutions":111,"../xmpp/SDPUtil":58,"./RTCBrowserType":10,"./adapter.screenshare":12}],12:[function(require,module,exports){ +},{"../../service/RTC/Resolutions":112,"../xmpp/SDPUtil":60,"./RTCBrowserType":10,"./adapter.screenshare":12}],12:[function(require,module,exports){ /*! adapterjs - v0.11.0 - 2015-06-08 */ // Adapter's interface. @@ -3403,6 +3416,7 @@ 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"); @@ -3551,13 +3565,13 @@ function registerListeners() { VideoLayout.setDeviceAvailabilityIcons(null, devices); }); APP.RTC.addListener(RTCEvents.VIDEO_MUTE, UI.setVideoMuteButtonsState); - APP.RTC.addListener(RTCEvents.DATA_CHANNEL_OPEN, function() { + 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 userJid = APP.UI.getLargeVideoJid(); - eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, userJid); + var userResource = APP.UI.getLargeVideoResource(); + eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, userResource); }); APP.statistics.addAudioLevelListener(function(jid, audioLevel) { var resourceJid; @@ -3571,7 +3585,7 @@ function registerListeners() { } AudioLevels.updateAudioLevel(resourceJid, audioLevel, - UI.getLargeVideoJid()); + UI.getLargeVideoResource()); }); APP.desktopsharing.addListener(function () { ToolbarToggler.showDesktopSharingButton(); @@ -3636,10 +3650,8 @@ function registerListeners() { 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.MESSAGE_RECEIVED, updateChatConversation); APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft); APP.xmpp.addListener(XMPPEvents.PASSWORD_REQUIRED, onPasswordRequired); - APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError); APP.xmpp.addListener(XMPPEvents.ETHERPAD, initEtherpad); APP.xmpp.addListener(XMPPEvents.AUTHENTICATION_REQUIRED, onAuthenticationRequired); @@ -3651,11 +3663,11 @@ function registerListeners() { APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED, VideoLayout.onAudioMute); APP.xmpp.addListener(XMPPEvents.VIDEO_MUTED, VideoLayout.onVideoMute); - APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS, function(doMuteAudio) { + APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS, function (doMuteAudio) { UI.setAudioMuted(doMuteAudio); }); APP.members.addListener(MemberEvents.DTMF_SUPPORT_CHANGED, - onDtmfSupportChanged); + onDtmfSupportChanged); APP.xmpp.addListener(XMPPEvents.START_MUTED_SETTING_CHANGED, function (audio, video) { SettingsMenu.setStartMuted(audio, video); }); @@ -3668,43 +3680,43 @@ function registerListeners() { "dialog.internalError"); }); - APP.xmpp.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR, function () { messageHandler.showError("dialog.error", - "dialog.SLDFailure"); + "dialog.SLDFailure"); }); - APP.xmpp.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR, function () { messageHandler.showError("dialog.error", "dialog.SRDFailure"); }); - APP.xmpp.addListener(XMPPEvents.CREATE_ANSWER_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.CREATE_ANSWER_ERROR, function () { messageHandler.showError(); }); - APP.xmpp.addListener(XMPPEvents.PROMPT_FOR_LOGIN, function() { + 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) { + + 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) { + + 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) { + APP.xmpp.addListener(XMPPEvents.ROOM_CONNECT_ERROR, function (pres) { UI.messageHandler.openReportDialog(null, "dialog.connectError", pres); }); - APP.xmpp.addListener(XMPPEvents.READY_TO_JOIN, function() { + 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); @@ -3714,10 +3726,14 @@ function registerListeners() { AudioLevels.init(); }); - // 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); + 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); + } } @@ -3767,9 +3783,6 @@ UI.start = function (init) { $("#welcome_page").hide(); - $("#videospace").mousemove(function () { - return ToolbarToggler.showToolbar(); - }); // Set the defaults for prompt dialogs. $.prompt.setDefaults({persistent: false}); @@ -3781,34 +3794,39 @@ UI.start = function (init) { bindEvents(); setupPrezi(); - setupToolbars(); - setupChat(); - + 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; - $("#downloadlog").click(function (event) { - dump(event.target); - }); - 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(); - - // Display notice message at the top of the toolbar - if (config.noticeMessage) { - $('#noticeText').text(config.noticeMessage); - $('#notice').css({display: 'block'}); - } if(config.requireDisplayName) { var currentSettings = Settings.getSettings(); @@ -3819,30 +3837,33 @@ UI.start = function (init) { init(); - 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 - }; + 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(); + + SettingsMenu.init(); + } }; @@ -3915,6 +3936,8 @@ function onLocalRoleChanged(jid, info, pres, isModerator) { Authentication.closeAuthenticationWindow(); messageHandler.notify(null, "notify.me", 'connected', "notify.moderator"); + + Toolbar.checkAutoRecord(); } } @@ -4049,8 +4072,8 @@ UI.inputDisplayNameHandler = function (value) { VideoLayout.inputDisplayNameHandler(value); }; -UI.getLargeVideoJid = function() { - return VideoLayout.getLargeVideoJid(); +UI.getLargeVideoResource = function () { + return VideoLayout.getLargeVideoResource(); }; UI.generateRoomName = function() { @@ -4248,7 +4271,7 @@ UI.setVideoMute = setVideoMute; module.exports = UI; -},{"../../service/RTC/RTCEvents":110,"../../service/RTC/StreamEventTypes":112,"../../service/UI/UIEvents":113,"../../service/connectionquality/CQEvents":115,"../../service/desktopsharing/DesktopSharingEventTypes":116,"../../service/members/Events":117,"../../service/xmpp/XMPPEvents":119,"../RTC/RTCBrowserType":10,"./../settings/Settings":49,"./audio_levels/AudioLevels.js":14,"./authentication/Authentication":16,"./avatar/Avatar":18,"./etherpad/Etherpad.js":19,"./prezi/Prezi.js":20,"./side_pannels/SidePanelToggler":22,"./side_pannels/chat/Chat.js":23,"./side_pannels/contactlist/ContactList":27,"./side_pannels/settings/SettingsMenu":28,"./toolbars/BottomToolbar":29,"./toolbars/Toolbar":30,"./toolbars/ToolbarToggler":31,"./util/MessageHandler":33,"./util/NicknameHandler":34,"./util/UIUtil":35,"./videolayout/VideoLayout.js":41,"./welcome_page/RoomnameGenerator":42,"./welcome_page/WelcomePage":43,"events":1}],14:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111,"../../service/RTC/StreamEventTypes":113,"../../service/UI/UIEvents":114,"../../service/connectionquality/CQEvents":116,"../../service/desktopsharing/DesktopSharingEventTypes":117,"../../service/members/Events":118,"../../service/xmpp/XMPPEvents":120,"../RTC/RTCBrowserType":10,"./../settings/Settings":49,"./audio_levels/AudioLevels.js":14,"./authentication/Authentication":16,"./avatar/Avatar":18,"./etherpad/Etherpad.js":19,"./prezi/Prezi.js":20,"./side_pannels/SidePanelToggler":22,"./side_pannels/chat/Chat.js":23,"./side_pannels/contactlist/ContactList":27,"./side_pannels/settings/SettingsMenu":28,"./toolbars/BottomToolbar":29,"./toolbars/Toolbar":30,"./toolbars/ToolbarToggler":31,"./util/JitsiPopover":32,"./util/MessageHandler":33,"./util/NicknameHandler":34,"./util/UIUtil":35,"./videolayout/VideoLayout.js":41,"./welcome_page/RoomnameGenerator":42,"./welcome_page/WelcomePage":43,"events":1}],14:[function(require,module,exports){ /* global APP, interfaceConfig, $, Strophe */ var CanvasUtil = require("./CanvasUtils"); @@ -4751,7 +4774,7 @@ var Authentication = { }; module.exports = Authentication; -},{"../../xmpp/moderator":61,"./LoginDialog":17}],17:[function(require,module,exports){ +},{"../../xmpp/moderator":62,"./LoginDialog":17}],17:[function(require,module,exports){ /* global $, APP, config*/ var XMPP = require('../../xmpp/xmpp'); @@ -4980,7 +5003,7 @@ var LoginDialog = { }; module.exports = LoginDialog; -},{"../../xmpp/moderator":61,"../../xmpp/xmpp":69}],18:[function(require,module,exports){ +},{"../../xmpp/moderator":62,"../../xmpp/xmpp":70}],18:[function(require,module,exports){ var Settings = require("../../settings/Settings"); var users = {}; @@ -6347,7 +6370,7 @@ var Chat = (function (my) { return my; }(Chat || {})); module.exports = Chat; -},{"../../../../service/UI/UIEvents":113,"../../toolbars/ToolbarToggler":31,"../../util/NicknameHandler":34,"../../util/UIUtil":35,"../SidePanelToggler":22,"./Commands":24,"./Replacement":25,"./smileys.json":26}],24:[function(require,module,exports){ +},{"../../../../service/UI/UIEvents":114,"../../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"); @@ -6852,7 +6875,7 @@ var SettingsMenu = { module.exports = SettingsMenu; -},{"../../../../service/translation/languages":118,"../../avatar/Avatar":18,"../../util/UIUtil":35,"./../../../settings/Settings":49}],29:[function(require,module,exports){ +},{"../../../../service/translation/languages":119,"../../avatar/Avatar":18,"../../util/UIUtil":35,"./../../../settings/Settings":49}],29:[function(require,module,exports){ /* global $ */ var PanelToggler = require("../side_pannels/SidePanelToggler"); @@ -6914,6 +6937,7 @@ var AuthenticationEvents var roomUrl = null; var sharedKey = ''; var UI = null; +var recordingToaster = null; var buttonHandlers = { "toolbar_button_mute": function () { @@ -7023,8 +7047,13 @@ function hangup() { * Starts or stops the recording for the conference. */ -function toggleRecording() { +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"); @@ -7048,7 +7077,7 @@ function toggleRecording() { function () { }, ':input:first' ); - }, Toolbar.setRecordingButtonState, Toolbar.setRecordingButtonState); + }, Toolbar.setRecordingButtonState); } /** @@ -7449,17 +7478,54 @@ var Toolbar = (function (my) { }; // Sets the state of the recording button - my.setRecordingButtonState = function (isRecording) { + my.setRecordingButtonState = function (recordingState) { var selector = $('#toolbar_button_record'); - if (isRecording) { + + if (recordingState === 'on') { selector.removeClass("icon-recEnable"); selector.addClass("icon-recEnable active"); - } else { + + $("#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) { @@ -7534,7 +7600,7 @@ var Toolbar = (function (my) { }(Toolbar || {})); module.exports = Toolbar; -},{"../../../service/authentication/AuthenticationEvents":114,"../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){ +},{"../../../service/authentication/AuthenticationEvents":115,"../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 */ @@ -7590,6 +7656,8 @@ var ToolbarToggler = { * Shows the main toolbar. */ showToolbar: function () { + if (interfaceConfig.filmStripOnly) + return; var header = $("#header"), bottomToolbar = $("#bottomToolbar"); if (!header.is(':visible') || !bottomToolbar.is(":visible")) { @@ -7625,6 +7693,9 @@ var ToolbarToggler = { * @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')) { @@ -7700,6 +7771,8 @@ var JitsiPopover = (function () { * Shows the popover */ JitsiPopover.prototype.show = function () { + if(!JitsiPopover.enabled) + return; this.createPopover(); this.popoverShown = true; }; @@ -7772,12 +7845,21 @@ var JitsiPopover = (function () { 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) { /** @@ -7951,8 +8033,19 @@ var messageHandler = (function(my) { 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 = '" + APP.translation.translateString(displayNameKey); } displayNameSpan += ""; - toastr.info( + return toastr.info( displayNameSpan + '
' + '', 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 || {})); @@ -8010,7 +8125,7 @@ var NicknameHandler = { }; module.exports = NicknameHandler; -},{"../../../service/UI/UIEvents":113}],35:[function(require,module,exports){ +},{"../../../service/UI/UIEvents":114}],35:[function(require,module,exports){ /* global $ */ /** * Created by hristo on 12/22/14. @@ -8948,6 +9063,14 @@ var LargeVideo = { 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; @@ -8960,18 +9083,8 @@ var LargeVideo = { var top = availableHeight / 2 - avatarSize / 4 * 3; $('#activeSpeaker').css('top', top); + this.VideoLayout.resizeVideoSpace(animate, isVisible, completeFunction); if(animate) { - $('#videospace').animate({ - right: window.innerWidth - availableWidth, - width: availableWidth, - height: availableHeight - }, - { - queue: false, - duration: 500, - complete: completeFunction - }); - $('#largeVideoContainer').animate({ width: availableWidth, height: availableHeight @@ -8981,8 +9094,6 @@ var LargeVideo = { duration: 500 }); } else { - $('#videospace').width(availableWidth); - $('#videospace').height(availableHeight); $('#largeVideoContainer').width(availableWidth); $('#largeVideoContainer').height(availableHeight); } @@ -9153,7 +9264,7 @@ var LargeVideo = { }; module.exports = LargeVideo; -},{"../../../service/UI/UIEvents":113,"../../RTC/RTCBrowserType":10,"../../xmpp/xmpp":69,"../avatar/Avatar":18,"../toolbars/ToolbarToggler":31,"../util/UIUtil":35}],38:[function(require,module,exports){ +},{"../../../service/UI/UIEvents":114,"../../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"); @@ -9442,85 +9553,96 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() { * @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 */ -RemoteVideo.prototype.addRemoteVideoMenu = function () { - var spanElement = document.createElement('span'); - spanElement.className = 'remotevideomenu'; - this.container.appendChild(spanElement); +if (!interfaceConfig.filmStripOnly) { + RemoteVideo.prototype.addRemoteVideoMenu = function () { + var spanElement = document.createElement('span'); + spanElement.className = 'remotevideomenu'; - var menuElement = document.createElement('i'); - menuElement.className = 'fa fa-angle-down'; - menuElement.title = 'Remote user controls'; - spanElement.appendChild(menuElement); + 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 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 muteMenuItem = document.createElement('li'); + var muteLinkItem = document.createElement('a'); - var mutedIndicator = ""; + 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'; + if (!this.isMuted) { + muteLinkItem.innerHTML = mutedIndicator + + "
"; + muteLinkItem.className = 'mutelink'; } else { - this.innerHTML = mutedIndicator + - "
"; - this.className = 'mutelink'; + 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")); }; - 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 @@ -10178,6 +10300,7 @@ 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'); @@ -10186,6 +10309,7 @@ var RemoteVideo = require("./RemoteVideo"); var LargeVideo = require("./LargeVideo"); var LocalVideo = require("./LocalVideo"); + var remoteVideos = {}; var remoteVideoTypes = {}; var localVideoThumbnail = null; @@ -10209,8 +10333,12 @@ var VideoLayout = (function (my) { my.init = function (emitter) { eventEmitter = emitter; localVideoThumbnail = new LocalVideo(VideoLayout); + if (interfaceConfig.filmStripOnly) { + LargeVideo.disable(); + } else { + LargeVideo.init(VideoLayout, emitter); + } - LargeVideo.init(VideoLayout, emitter); VideoLayout.resizeLargeVideoContainer(); }; @@ -10339,7 +10467,7 @@ var VideoLayout = (function (my) { } }; - my.getLargeVideoJid = function () { + my.getLargeVideoResource = function () { return LargeVideo.getResourceJid(); }; @@ -10367,7 +10495,7 @@ var VideoLayout = (function (my) { resourceJid) { if(focusedVideoResourceJid) { var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid); - if(oldSmallVideo) + if (oldSmallVideo && !interfaceConfig.filmStripOnly) oldSmallVideo.focus(false); } @@ -10394,7 +10522,7 @@ var VideoLayout = (function (my) { // Update focused/pinned interface. if (resourceJid) { - if(smallVideo) + if (smallVideo && !interfaceConfig.filmStripOnly) smallVideo.focus(true); if (!noPinnedEndpointChangedEvent) { @@ -10529,7 +10657,11 @@ var VideoLayout = (function (my) { * Resizes the large video container. */ my.resizeLargeVideoContainer = function () { - LargeVideo.resize(); + if(LargeVideo.isEnabled()) { + LargeVideo.resize(); + } else { + VideoLayout.resizeVideoSpace(); + } VideoLayout.resizeThumbnails(); LargeVideo.position(); }; @@ -10548,7 +10680,7 @@ var VideoLayout = (function (my) { if(animate) { $('#remoteVideos').animate({ - height: height + height: height + 2 // adds 2 px because of small video 1px border }, { queue: false, @@ -10573,7 +10705,7 @@ var VideoLayout = (function (my) { } else { // size videos so that while keeping AR and max height, we have a // nice fit - $('#remoteVideos').height(height); + $('#remoteVideos').height(height + 2);// adds 2 px because of small video 1px border $('#remoteVideos>span').width(width); $('#remoteVideos>span').height(height); @@ -10606,7 +10738,7 @@ var VideoLayout = (function (my) { var availableWidth = availableWinWidth / numvids; var aspectRatio = 16.0 / 9.0; var maxHeight = Math.min(160, availableHeight); - availableHeight = Math.min(maxHeight, availableWidth / aspectRatio); + availableHeight = Math.min(maxHeight, availableWidth / aspectRatio, window.innerHeight - 18); if (availableHeight < availableWidth / aspectRatio) { availableWidth = Math.floor(availableHeight * aspectRatio); } @@ -11061,6 +11193,37 @@ var VideoLayout = (function (my) { 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; @@ -11121,7 +11284,7 @@ var VideoLayout = (function (my) { }(VideoLayout || {})); module.exports = VideoLayout; -},{"../../../service/RTC/MediaStreamTypes":109,"../../../service/UI/UIEvents":113,"../../RTC/RTC":9,"../../RTC/RTCBrowserType":10,"../audio_levels/AudioLevels":14,"../prezi/Prezi":20,"../side_pannels/contactlist/ContactList":27,"./LargeVideo":37,"./LocalVideo":38,"./RemoteVideo":39}],42:[function(require,module,exports){ +},{"../../../service/RTC/MediaStreamTypes":110,"../../../service/UI/UIEvents":114,"../../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 = [ @@ -11400,7 +11563,7 @@ function setupWelcomePage() { module.exports = setupWelcomePage; },{"./RoomnameGenerator":42}],44:[function(require,module,exports){ -/* global $, $iq, config */ +/* global $, $iq, config, interfaceConfig */ var params = {}; function getConfigParamsFromUrl() { if(!location.hash) @@ -11419,22 +11582,31 @@ params = getConfigParamsFromUrl(); var URLProcessor = { setConfigParametersFromUrl: function () { - for(var k in params) - { - if(typeof k !== "string" || k.indexOf("config.") === -1) + for(var key in params) { + if(typeof key !== "string") continue; - var v = params[k]; - var confKey = k.substr(7); - if(config[confKey] && typeof config[confKey] !== typeof v) + 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 " + k + + console.warn("The type of " + key + " is wrong. That parameter won't be updated in config.js."); continue; } - config[confKey] = v; - + confObj[confKey] = value; } } @@ -11576,7 +11748,7 @@ var ConnectionQuality = { }; module.exports = ConnectionQuality; -},{"../../service/connectionquality/CQEvents":115,"../../service/xmpp/XMPPEvents":119,"events":1}],46:[function(require,module,exports){ +},{"../../service/connectionquality/CQEvents":116,"../../service/xmpp/XMPPEvents":120,"events":1}],46:[function(require,module,exports){ /* global $, alert, APP, changeLocalVideo, chrome, config, getConferenceHandler, getUserMediaWithConstraints */ /** @@ -11965,7 +12137,7 @@ module.exports = { }; -},{"../../service/RTC/RTCEvents":110,"../../service/desktopsharing/DesktopSharingEventTypes":116,"../RTC/RTCBrowserType":10,"../RTC/adapter.screenshare":12,"events":1}],47:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111,"../../service/desktopsharing/DesktopSharingEventTypes":117,"../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 = {}; @@ -12193,7 +12365,7 @@ var Members = { module.exports = Members; -},{"../../service/members/Events":117,"../../service/xmpp/XMPPEvents":119,"events":1}],49:[function(require,module,exports){ +},{"../../service/members/Events":118,"../../service/xmpp/XMPPEvents":120,"events":1}],49:[function(require,module,exports){ var email = ''; var displayName = ''; var userId; @@ -13339,7 +13511,7 @@ var statistics = { module.exports = statistics; -},{"../../service/RTC/RTCEvents":110,"../../service/RTC/StreamEventTypes.js":112,"../../service/xmpp/XMPPEvents":119,"./CallStats":50,"./LocalStatsCollector.js":51,"./RTPStatsCollector.js":52,"events":1}],54:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111,"../../service/RTC/StreamEventTypes.js":113,"../../service/xmpp/XMPPEvents":120,"./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"); @@ -13477,8 +13649,138 @@ module.exports = { } }; -},{"../../service/translation/languages":118,"../settings/Settings":49,"i18next-client":71}],55:[function(require,module,exports){ +},{"../../service/translation/languages":119,"../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"); @@ -13487,33 +13789,22 @@ var async = require("async"); var transform = require("sdp-transform"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var RTCBrowserType = require("../RTC/RTCBrowserType"); -var VideoSSRCHack = require("./VideoSSRCHack"); +var SSRCReplacement = require("./LocalSSRCReplacement"); // Jingle stuff -function JingleSession(me, sid, connection, service, eventEmitter) { - this.me = me; - this.sid = sid; - this.connection = connection; +function JingleSessionPC(me, sid, connection, service, eventEmitter) { + JingleSession.call(this, me, sid, connection, service, eventEmitter); this.initiator = null; this.responder = null; - this.isInitiator = null; this.peerjid = null; this.state = null; this.localSDP = null; this.remoteSDP = null; this.relayedStreams = []; - this.startTime = null; - this.stopTime = null; - this.media_constraints = null; this.pc_constraints = null; - this.ice_config = {}; - this.drip_container = []; - this.service = service; - this.eventEmitter = eventEmitter; this.usetrickle = true; this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718 - this.usedrip = false; // dripping is sending trickle candidates not one-by-one this.hadstuncandidate = false; this.hadturncandidate = false; @@ -13545,8 +13836,19 @@ function JingleSession(me, sid, connection, service, eventEmitter) { // stable and the ice connection state is connected. this.modifySourcesQueue.pause(); } +JingleSessionPC.prototype = JingleSession.prototype; +JingleSessionPC.prototype.constructor = JingleSessionPC; -JingleSession.prototype.updateModifySourcesQueue = function() { + +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') { @@ -13556,25 +13858,15 @@ JingleSession.prototype.updateModifySourcesQueue = function() { } }; -JingleSession.prototype.initiate = function (peerjid, isInitiator) { +JingleSessionPC.prototype.doInitialize = function () { var self = this; - if (this.state !== null) { - console.error('attempt to initiate on session ' + this.sid + - 'in state ' + this.state); - return; - } - this.isInitiator = isInitiator; - this.state = 'pending'; - this.initiator = isInitiator ? this.me : peerjid; - this.responder = !isInitiator ? this.me : peerjid; - this.peerjid = peerjid; + this.hadstuncandidate = false; this.hadturncandidate = false; this.lasticecandidate = false; this.isreconnect = false; - this.peerconnection - = new TraceablePeerConnection( + this.peerconnection = new TraceablePeerConnection( this.connection.jingle.ice_config, this.connection.jingle.pc_constraints, this); @@ -13615,7 +13907,6 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) { self.updateModifySourcesQueue(); switch (self.peerconnection.iceConnectionState) { case 'connected': - self.startTime = new Date(); // Informs interested parties that the connection has been restored. if (self.peerconnection.signalingState === 'stable' && self.isreconnect) @@ -13625,7 +13916,6 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) { break; case 'disconnected': self.isreconnect = true; - self.stopTime = new Date(); // Informs interested parties that the connection has been interrupted. if (self.peerconnection.signalingState === 'stable') self.eventEmitter.emit(XMPPEvents.CONNECTION_INTERRUPTED); @@ -13691,7 +13981,7 @@ function onIceConnectionStateChange(sid, session) { } } -JingleSession.prototype.accept = function () { +JingleSessionPC.prototype.accept = function () { this.state = 'active'; var pranswer = this.peerconnection.localDescription; @@ -13734,7 +14024,7 @@ JingleSession.prototype.accept = function () { //console.log('setLocalDescription success'); self.setLocalDescription(); - VideoSSRCHack.processSessionInit(accept); + SSRCReplacement.processSessionInit(accept); self.connection.sendIQ(accept, function () { @@ -13748,7 +14038,7 @@ JingleSession.prototype.accept = function () { reason: $(stanza).find('error :first')[0].tagName }:{}; error.source = 'answer'; - JingleSession.onJingleError(self.sid, error); + JingleSessionPC.onJingleError(self.sid, error); }, 10000); }, @@ -13759,7 +14049,7 @@ JingleSession.prototype.accept = function () { ); }; -JingleSession.prototype.terminate = function (reason) { +JingleSessionPC.prototype.terminate = function (reason) { this.state = 'ended'; this.reason = reason; this.peerconnection.close(); @@ -13769,11 +14059,11 @@ JingleSession.prototype.terminate = function (reason) { } }; -JingleSession.prototype.active = function () { +JingleSessionPC.prototype.active = function () { return this.state == 'active'; }; -JingleSession.prototype.sendIceCandidate = function (candidate) { +JingleSessionPC.prototype.sendIceCandidate = function (candidate) { var self = this; if (candidate && !this.lasticecandidate) { var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session); @@ -13819,7 +14109,6 @@ JingleSession.prototype.sendIceCandidate = function (candidate) { initiator: this.initiator, sid: this.sid}); this.localSDP = new SDP(this.peerconnection.localDescription.sdp); - var self = this; var sendJingle = function (ssrc) { if(!ssrc) ssrc = {}; @@ -13828,7 +14117,7 @@ JingleSession.prototype.sendIceCandidate = function (candidate) { self.initiator == self.me ? 'initiator' : 'responder', ssrc); - VideoSSRCHack.processSessionInit(init); + SSRCReplacement.processSessionInit(init); self.connection.sendIQ(init, function () { @@ -13845,10 +14134,10 @@ JingleSession.prototype.sendIceCandidate = function (candidate) { reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'offer'; - JingleSession.onJingleError(self.sid, error); + JingleSessionPC.onJingleError(self.sid, error); }, 10000); - } + }; sendJingle(); } this.lasticecandidate = true; @@ -13861,7 +14150,7 @@ JingleSession.prototype.sendIceCandidate = function (candidate) { } }; -JingleSession.prototype.sendIceCandidates = function (candidates) { +JingleSessionPC.prototype.sendIceCandidates = function (candidates) { console.log('sendIceCandidates', candidates); var cand = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', @@ -13910,13 +14199,13 @@ JingleSession.prototype.sendIceCandidates = function (candidates) { reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'transportinfo'; - JingleSession.onJingleError(this.sid, error); + JingleSessionPC.onJingleError(this.sid, error); }, 10000); }; -JingleSession.prototype.sendOffer = function () { +JingleSessionPC.prototype.sendOffer = function () { //console.log('sendOffer...'); var self = this; this.peerconnection.createOffer(function (sdp) { @@ -13930,7 +14219,7 @@ JingleSession.prototype.sendOffer = function () { }; // FIXME createdOffer is never used in jitsi-meet -JingleSession.prototype.createdOffer = function (sdp) { +JingleSessionPC.prototype.createdOffer = function (sdp) { //console.log('createdOffer', sdp); var self = this; this.localSDP = new SDP(sdp.sdp); @@ -13947,7 +14236,7 @@ JingleSession.prototype.createdOffer = function (sdp) { this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC); - VideoSSRCHack.processSessionInit(init); + SSRCReplacement.processSessionInit(init); self.connection.sendIQ(init, function () { @@ -13963,7 +14252,7 @@ JingleSession.prototype.createdOffer = function (sdp) { reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'offer'; - JingleSession.onJingleError(self.sid, error); + JingleSessionPC.onJingleError(self.sid, error); }, 10000); } @@ -13993,7 +14282,7 @@ JingleSession.prototype.createdOffer = function (sdp) { } }; -JingleSession.prototype.readSsrcInfo = function (contents) { +JingleSessionPC.prototype.readSsrcInfo = function (contents) { var self = this; $(contents).each(function (idx, content) { var name = $(content).attr('name'); @@ -14011,11 +14300,11 @@ JingleSession.prototype.readSsrcInfo = function (contents) { }); }; -JingleSession.prototype.getSsrcOwner = function (ssrc) { +JingleSessionPC.prototype.getSsrcOwner = function (ssrc) { return this.ssrcOwners[ssrc]; }; -JingleSession.prototype.setRemoteDescription = function (elem, desctype) { +JingleSessionPC.prototype.setRemoteDescription = function (elem, desctype) { //console.log('setting remote description... ', desctype); this.remoteSDP = new SDP(''); this.remoteSDP.fromJingle(elem); @@ -14055,12 +14344,12 @@ JingleSession.prototype.setRemoteDescription = function (elem, desctype) { }, function (e) { console.error('setRemoteDescription error', e); - JingleSession.onJingleFatalError(self, e); + JingleSessionPC.onJingleFatalError(self, e); } ); }; -JingleSession.prototype.addIceCandidate = function (elem) { +JingleSessionPC.prototype.addIceCandidate = function (elem) { var self = this; if (this.peerconnection.signalingState == 'closed') { return; @@ -14168,7 +14457,7 @@ JingleSession.prototype.addIceCandidate = function (elem) { }); }; -JingleSession.prototype.sendAnswer = function (provisional) { +JingleSessionPC.prototype.sendAnswer = function (provisional) { //console.log('createAnswer', provisional); var self = this; this.peerconnection.createAnswer( @@ -14183,7 +14472,7 @@ JingleSession.prototype.sendAnswer = function (provisional) { ); }; -JingleSession.prototype.createdAnswer = function (sdp, provisional) { +JingleSessionPC.prototype.createdAnswer = function (sdp, provisional) { //console.log('createAnswer callback'); var self = this; this.localSDP = new SDP(sdp.sdp); @@ -14213,7 +14502,7 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) { self.initiator == self.me ? 'initiator' : 'responder', ssrcs); - VideoSSRCHack.processSessionInit(accept); + SSRCReplacement.processSessionInit(accept); self.connection.sendIQ(accept, function () { @@ -14227,7 +14516,7 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) { reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'answer'; - JingleSession.onJingleError(self.sid, error); + JingleSessionPC.onJingleError(self.sid, error); }, 10000); } @@ -14257,7 +14546,7 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) { } }; -JingleSession.prototype.sendTerminate = function (reason, text) { +JingleSessionPC.prototype.sendTerminate = function (reason, text) { var self = this, term = $iq({to: this.peerjid, type: 'set'}) @@ -14295,7 +14584,7 @@ JingleSession.prototype.sendTerminate = function (reason, text) { } }; -JingleSession.prototype.addSource = function (elem, fromJid) { +JingleSessionPC.prototype.addSource = function (elem, fromJid) { var self = this; // FIXME: dirty waiting @@ -14377,7 +14666,7 @@ JingleSession.prototype.addSource = function (elem, fromJid) { }); }; -JingleSession.prototype.removeSource = function (elem, fromJid) { +JingleSessionPC.prototype.removeSource = function (elem, fromJid) { var self = this; // FIXME: dirty waiting @@ -14448,7 +14737,7 @@ JingleSession.prototype.removeSource = function (elem, fromJid) { }); }; -JingleSession.prototype._modifySources = function (successCallback, queueCallback) { +JingleSessionPC.prototype._modifySources = function (successCallback, queueCallback) { var self = this; if (this.peerconnection.signalingState == 'closed') return; @@ -14554,7 +14843,7 @@ JingleSession.prototype._modifySources = function (successCallback, queueCallbac * @param oldStream old video stream of this session. * @param success_callback callback executed after successful stream switch. */ -JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback, isAudio) { +JingleSessionPC.prototype.switchStreams = function (new_stream, oldStream, success_callback, isAudio) { var self = this; @@ -14592,7 +14881,7 @@ JingleSession.prototype.switchStreams = function (new_stream, oldStream, success * @param old_sdp SDP object for old description. * @param new_sdp SDP object for new description. */ -JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { +JingleSessionPC.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){ @@ -14615,9 +14904,10 @@ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { // Let 'source-remove' IQ through the hack and see if we're allowed to send // it in the current form if (removed) - remove = VideoSSRCHack.processSourceRemove(remove); + 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); @@ -14645,9 +14935,10 @@ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { // Let 'source-add' IQ through the hack and see if we're allowed to send // it in the current form if (added) - add = VideoSSRCHack.processSourceAdd(add); + add = SSRCReplacement.processSourceAdd(add); - if (added & add) { + if (added && add) { + console.info("Sending source-add", add); this.connection.sendIQ(add, function (res) { console.info('got add result', res); @@ -14675,7 +14966,7 @@ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { * specifies whether the method was initiated in response to a user command (in * contrast to an automatic decision made by the application logic) */ -JingleSession.prototype.setVideoMute = function (mute, callback, options) { +JingleSessionPC.prototype.setVideoMute = function (mute, callback, options) { var byUser; if (options) { @@ -14715,11 +15006,11 @@ JingleSession.prototype.setVideoMute = function (mute, callback, options) { }); }; -JingleSession.prototype.hardMuteVideo = function (muted) { +JingleSessionPC.prototype.hardMuteVideo = function (muted) { this.pendingop = muted ? 'mute' : 'unmute'; }; -JingleSession.prototype.sendMute = function (muted, content) { +JingleSessionPC.prototype.sendMute = function (muted, content) { var info = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', @@ -14734,7 +15025,7 @@ JingleSession.prototype.sendMute = function (muted, content) { this.connection.send(info); }; -JingleSession.prototype.sendRinging = function () { +JingleSessionPC.prototype.sendRinging = function () { var info = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', @@ -14745,7 +15036,7 @@ JingleSession.prototype.sendRinging = function () { this.connection.send(info); }; -JingleSession.prototype.getStats = function (interval) { +JingleSessionPC.prototype.getStats = function (interval) { var self = this; var recv = {audio: 0, video: 0}; var lost = {audio: 0, video: 0}; @@ -14791,12 +15082,12 @@ JingleSession.prototype.getStats = function (interval) { return this.statsinterval; }; -JingleSession.onJingleError = function (session, error) +JingleSessionPC.onJingleError = function (session, error) { console.error("Jingle error", error); } -JingleSession.onJingleFatalError = function (session, error) +JingleSessionPC.onJingleFatalError = function (session, error) { this.service.sessionTerminated = true; this.connection.emuc.doLeave(); @@ -14804,7 +15095,7 @@ JingleSession.onJingleFatalError = function (session, error) this.eventEmitter.emit(XMPPEvents.JINGLE_FATAL_ERROR, session, error); } -JingleSession.prototype.setLocalDescription = function () { +JingleSessionPC.prototype.setLocalDescription = function () { var self = this; var newssrcs = []; var session = transform.parse(this.peerconnection.localDescription.sdp); @@ -14879,7 +15170,7 @@ function sendKeyframe(pc) { } -JingleSession.prototype.remoteStreamAdded = function (data, times) { +JingleSessionPC.prototype.remoteStreamAdded = function (data, times) { var self = this; var thessrc; var streamId = APP.RTC.getStreamID(data.stream); @@ -14931,9 +15222,274 @@ JingleSession.prototype.remoteStreamAdded = function (data, times) { } } -module.exports = JingleSession; +module.exports = JingleSessionPC; -},{"../../service/xmpp/XMPPEvents":119,"../RTC/RTCBrowserType":10,"./SDP":56,"./SDPDiffer":57,"./SDPUtil":58,"./TraceablePeerConnection":59,"./VideoSSRCHack":60,"async":70,"sdp-transform":106}],56:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120,"../RTC/RTCBrowserType":10,"./JingleSession":55,"./LocalSSRCReplacement":57,"./SDP":58,"./SDPDiffer":59,"./SDPUtil":60,"./TraceablePeerConnection":61,"async":71,"sdp-transform":107}],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 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"); @@ -15559,7 +16115,7 @@ SDP.prototype.jingle2media = function (content) { module.exports = SDP; -},{"./SDPUtil":58}],57:[function(require,module,exports){ +},{"./SDPUtil":60}],59:[function(require,module,exports){ var SDPUtil = require("./SDPUtil"); @@ -15731,7 +16287,7 @@ SDPDiffer.prototype.toJingle = function(modify) { }; module.exports = SDPDiffer; -},{"./SDPUtil":58}],58:[function(require,module,exports){ +},{"./SDPUtil":60}],60:[function(require,module,exports){ SDPUtil = { filter_special_chars: function (text) { return text.replace(/[\\\/\{,\}\+]/g, ""); @@ -16084,11 +16640,11 @@ SDPUtil = { } }; module.exports = SDPUtil; -},{}],59:[function(require,module,exports){ +},{}],61:[function(require,module,exports){ var RTC = require('../RTC/RTC'); var RTCBrowserType = require("../RTC/RTCBrowserType.js"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); -var VideoSSRCHack = require("./VideoSSRCHack"); +var SSRCReplacement = require("./LocalSSRCReplacement"); function TraceablePeerConnection(ice_config, constraints, session) { var self = this; @@ -16300,7 +16856,7 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; - desc = VideoSSRCHack.mungeLocalVideoSSRC(desc); + desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); @@ -16459,7 +17015,7 @@ TraceablePeerConnection.prototype.createOffer self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer)); } - offer = VideoSSRCHack.mungeLocalVideoSSRC(offer); + offer = SSRCReplacement.mungeLocalVideoSSRC(offer); if (config.enableSimulcast && self.simulcast.isSupported()) { offer = self.simulcast.mungeLocalDescription(offer); @@ -16489,7 +17045,7 @@ TraceablePeerConnection.prototype.createAnswer } // munge local video SSRC - answer = VideoSSRCHack.mungeLocalVideoSSRC(answer); + answer = SSRCReplacement.mungeLocalVideoSSRC(answer); if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer); @@ -16539,178 +17095,7 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) { module.exports = TraceablePeerConnection; -},{"../../service/xmpp/XMPPEvents":119,"../RTC/RTC":9,"../RTC/RTCBrowserType.js":10,"./VideoSSRCHack":60,"sdp-interop":92,"sdp-simulcast":99,"sdp-transform":106}],60:[function(require,module,exports){ -/* global $ */ - -/* - The purpose of this hack is to re-use SSRC of first video stream ever created - for any video streams created later on. In order to do that this hack: - - 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'); - -/** - * Stored SSRC of local video stream. - */ -var localVideoSSRC; - -/** - * 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) { - if (!localVideoSSRC) - return modifyIq; - - var modifyIqTree = $(modifyIq.tree()); - var videoSSRC = modifyIqTree.find( - '>jingle>content[name="video"]' + - '>description>source[ssrc="' + localVideoSSRC + '"]'); - - if (!videoSSRC.length) { - return modifyIqTree; - } - - 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; - } 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'); - - console.info('Video desc: ', videoSSRCs); - if (!videoSSRCs.length) - return; - - var ssrc = videoSSRCs.attr('ssrc'); - if (ssrc) { - localVideoSSRC = ssrc; - console.info( - 'Stored local video SSRC for future re-use: ' + localVideoSSRC); - } else { - console.error('No "ssrc" attribute present in element'); - } -}; - -var LocalVideoSSRCHack = { - /** - * 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 (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 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); - } - } - 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 (!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) { - return filterOutSource(sourceRemove, 'source-remove'); - } -}; - -module.exports = LocalVideoSSRCHack; - -},{"./SDP":56}],61:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120,"../RTC/RTC":9,"../RTC/RTCBrowserType.js":10,"./LocalSSRCReplacement":57,"sdp-interop":93,"sdp-simulcast":100,"sdp-transform":107}],62:[function(require,module,exports){ /* global $, $iq, APP, config, messageHandler, roomName, sessionTerminated, Strophe, Util */ var XMPPEvents = require("../../service/xmpp/XMPPEvents"); @@ -17143,7 +17528,7 @@ module.exports = Moderator; -},{"../../service/authentication/AuthenticationEvents":114,"../../service/xmpp/XMPPEvents":119,"../settings/Settings":49}],62:[function(require,module,exports){ +},{"../../service/authentication/AuthenticationEvents":115,"../../service/xmpp/XMPPEvents":120,"../settings/Settings":49}],63:[function(require,module,exports){ /* global $, $iq, config, connection, focusMucJid, messageHandler, Toolbar, Util */ var Moderator = require("./moderator"); @@ -17165,6 +17550,12 @@ var useJirecon = (typeof config.hosts.jirecon != "undefined"); */ 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; } @@ -17176,9 +17567,9 @@ function setRecordingJirecon(state, token, callback, connection) { var iq = $iq({to: config.hosts.jirecon, type: 'set'}) .c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon', - action: state ? 'start' : 'stop', + action: (state === 'on') ? 'start' : 'stop', mucjid: connection.emuc.roomjid}); - if (!state){ + if (state === 'off'){ iq.attrs({rid: jireconRid}); } @@ -17190,10 +17581,10 @@ function setRecordingJirecon(state, token, callback, connection) { // TODO wait for an IQ with the real status, since this is // provisional? jireconRid = $(result).find('recording').attr('rid'); - console.log('Recording ' + (state ? 'started' : 'stopped') + + console.log('Recording ' + ((state === 'on') ? 'started' : 'stopped') + '(jirecon)' + result); recordingEnabled = state; - if (!state){ + if (state === 'off'){ jireconRid = null; } @@ -17219,10 +17610,19 @@ function setRecordingColibri(state, token, callback, connection) { function (result) { console.log('Set recording "', state, '". Result:', result); var recordingElem = $(result).find('>conference>recording'); - var newState = ('true' === recordingElem.attr('state')); + 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); @@ -17240,8 +17640,7 @@ function setRecording(state, token, callback, connection) { } var Recording = { - toggleRecording: function (tokenEmptyCallback, - startingCallback, startedCallback, connection) { + toggleRecording: function (tokenEmptyCallback, recordingStateChangeCallback, connection) { if (!Moderator.isModerator()) { console.log( 'non-focus, or conference not yet organized:' + @@ -17254,16 +17653,16 @@ var Recording = { if (!recordingToken && !useJirecon) { tokenEmptyCallback(function (value) { setRecordingToken(value); - self.toggleRecording(tokenEmptyCallback, - startingCallback, startedCallback, connection); + self.toggleRecording(tokenEmptyCallback, recordingStateChangeCallback, connection); }); return; } var oldState = recordingEnabled; - startingCallback(!oldState); - setRecording(!oldState, + var newState = (oldState === 'off' || !oldState) ? 'on' : 'off'; + + setRecording(newState, recordingToken, function (state) { console.log("New recording state: ", state); @@ -17289,7 +17688,7 @@ var Recording = { // have been wrong setRecordingToken(null); } - startedCallback(state); + recordingStateChangeCallback(state); }, connection @@ -17299,7 +17698,7 @@ var Recording = { }; module.exports = Recording; -},{"./moderator":61}],63:[function(require,module,exports){ +},{"./moderator":62}],64:[function(require,module,exports){ /* jshint -W117 */ /* a simple MUC connection plugin * can only handle a single MUC room @@ -17941,31 +18340,15 @@ module.exports = function(XMPP, eventEmitter) { }; -},{"../../service/xmpp/XMPPEvents":119,"./moderator":61}],64:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120,"./moderator":62}],65:[function(require,module,exports){ /* jshint -W117 */ -var JingleSession = require("./JingleSession"); +var JingleSession = require("./JingleSessionPC"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var RTCBrowserType = require("../RTC/RTCBrowserType"); module.exports = function(XMPP, eventEmitter) { - function CallIncomingJingle(sid, connection) { - var sess = connection.jingle.sessions[sid]; - - // TODO: do we check activecall == null? - connection.jingle.activecall = sess; - - eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess); - - // TODO: check affiliation and/or role - console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); - sess.usedrip = true; // not-so-naive trickle ice - sess.sendAnswer(); - sess.accept(); - - } - Strophe.addConnectionPlugin('jingle', { connection: null, sessions: {}, @@ -18070,20 +18453,30 @@ module.exports = function(XMPP, eventEmitter) { sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; - sess.initiate(fromJid, false); + sess.initialize(fromJid, false); // FIXME: setRemoteDescription should only be done when this call is to be accepted - sess.setRemoteDescription($(iq).find('>jingle'), 'offer'); + 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 synchronus - CallIncomingJingle(sess.sid, this.connection); + // 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.setRemoteDescription($(iq).find('>jingle'), 'answer'); + sess.setAnswer($(iq).find('>jingle')); sess.accept(); $(document).trigger('callaccepted.jingle', [sess.sid]); break; @@ -18150,7 +18543,7 @@ module.exports = function(XMPP, eventEmitter) { sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; - sess.initiate(peerjid, true); + sess.initialize(peerjid, true); this.sessions[sess.sid] = sess; this.jid2session[sess.peerjid] = sess; sess.sendOffer(); @@ -18290,7 +18683,7 @@ module.exports = function(XMPP, eventEmitter) { }; -},{"../../service/xmpp/XMPPEvents":119,"../RTC/RTCBrowserType":10,"./JingleSession":55}],65:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120,"../RTC/RTCBrowserType":10,"./JingleSessionPC":56}],66:[function(require,module,exports){ /* global Strophe */ module.exports = function () { @@ -18311,7 +18704,7 @@ module.exports = function () { } }); }; -},{}],66:[function(require,module,exports){ +},{}],67:[function(require,module,exports){ /* global $, $iq, config, connection, focusMucJid, forceMuted, setAudioMuted, Strophe */ /** @@ -18372,7 +18765,7 @@ module.exports = function (XMPP, eventEmitter) { } }); } -},{"../../service/xmpp/XMPPEvents":119}],67:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":120}],68:[function(require,module,exports){ /* jshint -W117 */ module.exports = function() { Strophe.addConnectionPlugin('rayo', @@ -18469,7 +18862,7 @@ module.exports = function() { ); }; -},{}],68:[function(require,module,exports){ +},{}],69:[function(require,module,exports){ /** * Strophe logger implementation. Logs from level WARN and above. */ @@ -18513,7 +18906,7 @@ module.exports = function () { }; }; -},{}],69:[function(require,module,exports){ +},{}],70:[function(require,module,exports){ /* global $, APP, config, Strophe*/ var Moderator = require("./moderator"); var EventEmitter = require("events"); @@ -18950,9 +19343,9 @@ var XMPP = { return true; }, toggleRecording: function (tokenEmptyCallback, - startingCallback, startedCallback) { + recordingStateChangeCallback) { Recording.toggleRecording(tokenEmptyCallback, - startingCallback, startedCallback, connection); + recordingStateChangeCallback, connection); }, addToPresence: function (name, value, dontSend) { switch (name) { @@ -19083,7 +19476,7 @@ var XMPP = { module.exports = XMPP; -},{"../../service/RTC/RTCEvents":110,"../../service/RTC/StreamEventTypes":112,"../../service/xmpp/XMPPEvents":119,"../settings/Settings":49,"./SDP":56,"./moderator":61,"./recording":62,"./strophe.emuc":63,"./strophe.jingle":64,"./strophe.logger":65,"./strophe.moderate":66,"./strophe.rayo":67,"./strophe.util":68,"events":1,"pako":72,"retry":88}],70:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":111,"../../service/RTC/StreamEventTypes":113,"../../service/xmpp/XMPPEvents":120,"../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":73,"retry":89}],71:[function(require,module,exports){ (function (process){ /*! * async @@ -20210,7 +20603,7 @@ module.exports = XMPP; }()); }).call(this,require('_process')) -},{"_process":2}],71:[function(require,module,exports){ +},{"_process":2}],72:[function(require,module,exports){ // i18next, v1.7.7 // Copyright (c)2014 Jan Mühlemann (jamuhl). // Distributed under MIT license @@ -22333,7 +22726,7 @@ module.exports = XMPP; i18n.options = o; })(); -},{"jquery":"jquery"}],72:[function(require,module,exports){ +},{"jquery":"jquery"}],73:[function(require,module,exports){ // Top level file is just a mixin of submodules & constants 'use strict'; @@ -22349,7 +22742,7 @@ assign(pako, deflate, inflate, constants); module.exports = pako; -},{"./lib/deflate":73,"./lib/inflate":74,"./lib/utils/common":75,"./lib/zlib/constants":78}],73:[function(require,module,exports){ +},{"./lib/deflate":74,"./lib/inflate":75,"./lib/utils/common":76,"./lib/zlib/constants":79}],74:[function(require,module,exports){ 'use strict'; @@ -22727,7 +23120,7 @@ exports.deflate = deflate; exports.deflateRaw = deflateRaw; exports.gzip = gzip; -},{"./utils/common":75,"./utils/strings":76,"./zlib/deflate.js":80,"./zlib/messages":85,"./zlib/zstream":87}],74:[function(require,module,exports){ +},{"./utils/common":76,"./utils/strings":77,"./zlib/deflate.js":81,"./zlib/messages":86,"./zlib/zstream":88}],75:[function(require,module,exports){ 'use strict'; @@ -23108,7 +23501,7 @@ exports.inflate = inflate; exports.inflateRaw = inflateRaw; exports.ungzip = inflate; -},{"./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){ +},{"./utils/common":76,"./utils/strings":77,"./zlib/constants":79,"./zlib/gzheader":82,"./zlib/inflate.js":84,"./zlib/messages":86,"./zlib/zstream":88}],76:[function(require,module,exports){ 'use strict'; @@ -23212,7 +23605,7 @@ exports.setTyped = function (on) { exports.setTyped(TYPED_OK); -},{}],76:[function(require,module,exports){ +},{}],77:[function(require,module,exports){ // String encode/decode helpers 'use strict'; @@ -23399,7 +23792,7 @@ exports.utf8border = function(buf, max) { return (pos + _utf8len[buf[pos]] > max) ? pos : max; }; -},{"./common":75}],77:[function(require,module,exports){ +},{"./common":76}],78:[function(require,module,exports){ 'use strict'; // Note: adler32 takes 12% for level 0 and 2% for level 6. @@ -23433,7 +23826,7 @@ function adler32(adler, buf, len, pos) { module.exports = adler32; -},{}],78:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ module.exports = { /* Allowed flush values; see deflate() and inflate() below for details */ @@ -23482,7 +23875,7 @@ module.exports = { //Z_NULL: null // Use -1 or null inline, depending on var type }; -},{}],79:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ 'use strict'; // Note: we can't get significant speed boost here. @@ -23525,7 +23918,7 @@ function crc32(crc, buf, len, pos) { module.exports = crc32; -},{}],80:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ 'use strict'; var utils = require('../utils/common'); @@ -25292,7 +25685,7 @@ exports.deflatePrime = deflatePrime; exports.deflateTune = deflateTune; */ -},{"../utils/common":75,"./adler32":77,"./crc32":79,"./messages":85,"./trees":86}],81:[function(require,module,exports){ +},{"../utils/common":76,"./adler32":78,"./crc32":80,"./messages":86,"./trees":87}],82:[function(require,module,exports){ 'use strict'; @@ -25334,7 +25727,7 @@ function GZheader() { module.exports = GZheader; -},{}],82:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ 'use strict'; // See state defs from inflate.js @@ -25661,7 +26054,7 @@ module.exports = function inflate_fast(strm, start) { return; }; -},{}],83:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ 'use strict'; @@ -27166,7 +27559,7 @@ exports.inflateSyncPoint = inflateSyncPoint; exports.inflateUndermine = inflateUndermine; */ -},{"../utils/common":75,"./adler32":77,"./crc32":79,"./inffast":82,"./inftrees":84}],84:[function(require,module,exports){ +},{"../utils/common":76,"./adler32":78,"./crc32":80,"./inffast":83,"./inftrees":85}],85:[function(require,module,exports){ 'use strict'; @@ -27495,7 +27888,7 @@ module.exports = function inflate_table(type, lens, lens_index, codes, table, ta return 0; }; -},{"../utils/common":75}],85:[function(require,module,exports){ +},{"../utils/common":76}],86:[function(require,module,exports){ 'use strict'; module.exports = { @@ -27510,7 +27903,7 @@ module.exports = { '-6': 'incompatible version' /* Z_VERSION_ERROR (-6) */ }; -},{}],86:[function(require,module,exports){ +},{}],87:[function(require,module,exports){ 'use strict'; @@ -28711,7 +29104,7 @@ exports._tr_flush_block = _tr_flush_block; exports._tr_tally = _tr_tally; exports._tr_align = _tr_align; -},{"../utils/common":75}],87:[function(require,module,exports){ +},{"../utils/common":76}],88:[function(require,module,exports){ 'use strict'; @@ -28742,9 +29135,9 @@ function ZStream() { module.exports = ZStream; -},{}],88:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ module.exports = require('./lib/retry'); -},{"./lib/retry":89}],89:[function(require,module,exports){ +},{"./lib/retry":90}],90:[function(require,module,exports){ var RetryOperation = require('./retry_operation'); exports.operation = function(options) { @@ -28795,7 +29188,7 @@ exports._createTimeout = function(attempt, opts) { return timeout; }; -},{"./retry_operation":90}],90:[function(require,module,exports){ +},{"./retry_operation":91}],91:[function(require,module,exports){ function RetryOperation(timeouts) { this._timeouts = timeouts; this._fn = null; @@ -28905,7 +29298,7 @@ RetryOperation.prototype.mainError = function() { return mainError; }; -},{}],91:[function(require,module,exports){ +},{}],92:[function(require,module,exports){ module.exports = function arrayEquals(array) { // if the other array is a falsy value, return if (!array) @@ -28931,10 +29324,10 @@ module.exports = function arrayEquals(array) { } -},{}],92:[function(require,module,exports){ +},{}],93:[function(require,module,exports){ exports.Interop = require('./interop'); -},{"./interop":93}],93:[function(require,module,exports){ +},{"./interop":94}],94:[function(require,module,exports){ "use strict"; var transform = require('./transform'); @@ -29516,7 +29909,7 @@ Interop.prototype.toUnifiedPlan = function(desc) { //#endregion }; -},{"./array-equals":91,"./transform":94}],94:[function(require,module,exports){ +},{"./array-equals":92,"./transform":95}],95:[function(require,module,exports){ var transform = require('sdp-transform'); exports.write = function(session, opts) { @@ -29615,7 +30008,7 @@ exports.parse = function(sdp) { }; -},{"sdp-transform":96}],95:[function(require,module,exports){ +},{"sdp-transform":97}],96:[function(require,module,exports){ var grammar = module.exports = { v: [{ name: 'version', @@ -29864,7 +30257,7 @@ Object.keys(grammar).forEach(function (key) { }); }); -},{}],96:[function(require,module,exports){ +},{}],97:[function(require,module,exports){ var parser = require('./parser'); var writer = require('./writer'); @@ -29874,7 +30267,7 @@ exports.parseFmtpConfig = parser.parseFmtpConfig; exports.parsePayloads = parser.parsePayloads; exports.parseRemoteCandidates = parser.parseRemoteCandidates; -},{"./parser":97,"./writer":98}],97:[function(require,module,exports){ +},{"./parser":98,"./writer":99}],98:[function(require,module,exports){ var toIntIfInt = function (v) { return String(Number(v)) === v ? Number(v) : v; }; @@ -29969,7 +30362,7 @@ exports.parseRemoteCandidates = function (str) { return candidates; }; -},{"./grammar":95}],98:[function(require,module,exports){ +},{"./grammar":96}],99:[function(require,module,exports){ var grammar = require('./grammar'); // customized util.format - discards excess arguments and can void middle ones @@ -30085,7 +30478,7 @@ module.exports = function (session, opts) { return sdp.join('\r\n') + '\r\n'; }; -},{"./grammar":95}],99:[function(require,module,exports){ +},{"./grammar":96}],100:[function(require,module,exports){ var transform = require('sdp-transform'); var transformUtils = require('./transform-utils'); var parseSsrcs = transformUtils.parseSsrcs; @@ -30487,7 +30880,7 @@ Simulcast.prototype.mungeLocalDescription = function (desc) { module.exports = Simulcast; -},{"./transform-utils":100,"sdp-transform":102}],100:[function(require,module,exports){ +},{"./transform-utils":101,"sdp-transform":103}],101:[function(require,module,exports){ exports.writeSsrcs = function(sources, order) { var ssrcs = []; @@ -30538,30 +30931,30 @@ exports.parseSsrcs = function (mLine) { }; -},{}],101:[function(require,module,exports){ -arguments[4][95][0].apply(exports,arguments) -},{"dup":95}],102:[function(require,module,exports){ +},{}],102:[function(require,module,exports){ arguments[4][96][0].apply(exports,arguments) -},{"./parser":103,"./writer":104,"dup":96}],103:[function(require,module,exports){ +},{"dup":96}],103:[function(require,module,exports){ arguments[4][97][0].apply(exports,arguments) -},{"./grammar":101,"dup":97}],104:[function(require,module,exports){ +},{"./parser":104,"./writer":105,"dup":97}],104:[function(require,module,exports){ arguments[4][98][0].apply(exports,arguments) -},{"./grammar":101,"dup":98}],105:[function(require,module,exports){ -arguments[4][95][0].apply(exports,arguments) -},{"dup":95}],106:[function(require,module,exports){ +},{"./grammar":102,"dup":98}],105:[function(require,module,exports){ +arguments[4][99][0].apply(exports,arguments) +},{"./grammar":102,"dup":99}],106:[function(require,module,exports){ arguments[4][96][0].apply(exports,arguments) -},{"./parser":107,"./writer":108,"dup":96}],107:[function(require,module,exports){ +},{"dup":96}],107:[function(require,module,exports){ arguments[4][97][0].apply(exports,arguments) -},{"./grammar":105,"dup":97}],108:[function(require,module,exports){ +},{"./parser":108,"./writer":109,"dup":97}],108:[function(require,module,exports){ arguments[4][98][0].apply(exports,arguments) -},{"./grammar":105,"dup":98}],109:[function(require,module,exports){ +},{"./grammar":106,"dup":98}],109:[function(require,module,exports){ +arguments[4][99][0].apply(exports,arguments) +},{"./grammar":106,"dup":99}],110:[function(require,module,exports){ var MediaStreamType = { VIDEO_TYPE: "Video", AUDIO_TYPE: "Audio" }; module.exports = MediaStreamType; -},{}],110:[function(require,module,exports){ +},{}],111:[function(require,module,exports){ var RTCEvents = { RTC_READY: "rtc.ready", DATA_CHANNEL_OPEN: "rtc.data_channel_open", @@ -30574,7 +30967,7 @@ var RTCEvents = { }; module.exports = RTCEvents; -},{}],111:[function(require,module,exports){ +},{}],112:[function(require,module,exports){ var Resolutions = { "1080": { width: 1920, @@ -30628,7 +31021,7 @@ var Resolutions = { } }; module.exports = Resolutions; -},{}],112:[function(require,module,exports){ +},{}],113:[function(require,module,exports){ var StreamEventTypes = { EVENT_TYPE_LOCAL_CREATED: "stream.local_created", @@ -30642,7 +31035,7 @@ var StreamEventTypes = { }; module.exports = StreamEventTypes; -},{}],113:[function(require,module,exports){ +},{}],114:[function(require,module,exports){ var UIEvents = { NICKNAME_CHANGED: "UI.nickname_changed", SELECTED_ENDPOINT: "UI.selected_endpoint", @@ -30650,7 +31043,7 @@ var UIEvents = { LARGEVIDEO_INIT: "UI.largevideo_init" }; module.exports = UIEvents; -},{}],114:[function(require,module,exports){ +},{}],115:[function(require,module,exports){ var AuthenticationEvents = { /** * Event callback arguments: @@ -30664,7 +31057,7 @@ var AuthenticationEvents = { }; module.exports = AuthenticationEvents; -},{}],115:[function(require,module,exports){ +},{}],116:[function(require,module,exports){ var CQEvents = { LOCALSTATS_UPDATED: "cq.localstats_updated", REMOTESTATS_UPDATED: "cq.remotestats_updated", @@ -30672,7 +31065,7 @@ var CQEvents = { }; module.exports = CQEvents; -},{}],116:[function(require,module,exports){ +},{}],117:[function(require,module,exports){ var DesktopSharingEventTypes = { INIT: "ds.init", @@ -30682,14 +31075,14 @@ var DesktopSharingEventTypes = { }; module.exports = DesktopSharingEventTypes; -},{}],117:[function(require,module,exports){ +},{}],118:[function(require,module,exports){ var Events = { DTMF_SUPPORT_CHANGED: "members.dtmf_support_changed" }; module.exports = Events; -},{}],118:[function(require,module,exports){ +},{}],119:[function(require,module,exports){ module.exports = { getLanguages : function () { var languages = []; @@ -30706,7 +31099,7 @@ module.exports = { TR: "tr", FR: "fr" } -},{}],119:[function(require,module,exports){ +},{}],120:[function(require,module,exports){ var XMPPEvents = { CONNECTION_FAILED: "xmpp.connection.failed", // Indicates an interrupted connection event. diff --git a/modules/RTC/LocalStream.js b/modules/RTC/LocalStream.js index 0722ceb87..acd18ac50 100644 --- a/modules/RTC/LocalStream.js +++ b/modules/RTC/LocalStream.js @@ -1,6 +1,24 @@ /* 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; @@ -21,9 +39,12 @@ function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) { }; } - this.stream.onended = function() { + this.stream.onended = function () { self.streamEnded(); }; + if (RTCBrowserType.isFirefox()) { + implementOnEndedHandling(this.stream); + } } LocalStream.prototype.streamEnded = function () { @@ -45,9 +66,11 @@ LocalStream.prototype.setMute = function (mute) var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE; if ((window.location.protocol != "https:" && this.isGUMStream) || - (isAudio && this.isGUMStream) || this.videoType === "screen") { - var tracks = this.getTracks(); + (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; } diff --git a/modules/RTC/RTCUtils.js b/modules/RTC/RTCUtils.js index b84354ff5..dbd1331d0 100644 --- a/modules/RTC/RTCUtils.js +++ b/modules/RTC/RTCUtils.js @@ -23,21 +23,17 @@ function getPreviousResolution(resolution) { } function setResolutionConstraints(constraints, resolution, isAndroid) { - if (resolution && !constraints.video || isAndroid) { - // same behaviour as true - constraints.video = { mandatory: {}, optional: [] }; - } - if(Resolutions[resolution]) { + if (Resolutions[resolution]) { constraints.video.mandatory.minWidth = Resolutions[resolution].width; constraints.video.mandatory.minHeight = Resolutions[resolution].height; } - else { - if (isAndroid) { - constraints.video.mandatory.minWidth = 320; - constraints.video.mandatory.minHeight = 240; - constraints.video.mandatory.maxFrameRate = 15; - } + 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) @@ -55,10 +51,28 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid if (um.indexOf('video') >= 0) { // same behaviour as true constraints.video = { mandatory: {}, optional: [] }; + + constraints.video.optional.push({ googLeakyBucket: true }); + + setResolutionConstraints(constraints, resolution, isAndroid); } if (um.indexOf('audio') >= 0) { - // same behaviour as true - constraints.audio = { mandatory: {}, optional: []}; + 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()) { @@ -100,30 +114,6 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid }; } - if (constraints.audio) { - // 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} - ); - } - if (constraints.video) { - if (um.indexOf('video') >= 0) { - constraints.video.optional.push( - {googLeakyBucket: true} - ); - } - } - - if (um.indexOf('video') >= 0) { - setResolutionConstraints(constraints, resolution, isAndroid); - } - if (bandwidth) { if (!constraints.video) { //same behaviour as true diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 4c57d1865..1f9e5375a 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -21,6 +21,7 @@ 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"); @@ -169,13 +170,13 @@ function registerListeners() { VideoLayout.setDeviceAvailabilityIcons(null, devices); }); APP.RTC.addListener(RTCEvents.VIDEO_MUTE, UI.setVideoMuteButtonsState); - APP.RTC.addListener(RTCEvents.DATA_CHANNEL_OPEN, function() { + 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 userJid = APP.UI.getLargeVideoJid(); - eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, userJid); + var userResource = APP.UI.getLargeVideoResource(); + eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, userResource); }); APP.statistics.addAudioLevelListener(function(jid, audioLevel) { var resourceJid; @@ -189,7 +190,7 @@ function registerListeners() { } AudioLevels.updateAudioLevel(resourceJid, audioLevel, - UI.getLargeVideoJid()); + UI.getLargeVideoResource()); }); APP.desktopsharing.addListener(function () { ToolbarToggler.showDesktopSharingButton(); @@ -254,10 +255,8 @@ function registerListeners() { 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.MESSAGE_RECEIVED, updateChatConversation); APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft); APP.xmpp.addListener(XMPPEvents.PASSWORD_REQUIRED, onPasswordRequired); - APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError); APP.xmpp.addListener(XMPPEvents.ETHERPAD, initEtherpad); APP.xmpp.addListener(XMPPEvents.AUTHENTICATION_REQUIRED, onAuthenticationRequired); @@ -269,11 +268,11 @@ function registerListeners() { APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED, VideoLayout.onAudioMute); APP.xmpp.addListener(XMPPEvents.VIDEO_MUTED, VideoLayout.onVideoMute); - APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS, function(doMuteAudio) { + APP.xmpp.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS, function (doMuteAudio) { UI.setAudioMuted(doMuteAudio); }); APP.members.addListener(MemberEvents.DTMF_SUPPORT_CHANGED, - onDtmfSupportChanged); + onDtmfSupportChanged); APP.xmpp.addListener(XMPPEvents.START_MUTED_SETTING_CHANGED, function (audio, video) { SettingsMenu.setStartMuted(audio, video); }); @@ -286,43 +285,43 @@ function registerListeners() { "dialog.internalError"); }); - APP.xmpp.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR, function () { messageHandler.showError("dialog.error", - "dialog.SLDFailure"); + "dialog.SLDFailure"); }); - APP.xmpp.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR, function () { messageHandler.showError("dialog.error", "dialog.SRDFailure"); }); - APP.xmpp.addListener(XMPPEvents.CREATE_ANSWER_ERROR, function() { + APP.xmpp.addListener(XMPPEvents.CREATE_ANSWER_ERROR, function () { messageHandler.showError(); }); - APP.xmpp.addListener(XMPPEvents.PROMPT_FOR_LOGIN, function() { + 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) { + + 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) { + + 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) { + APP.xmpp.addListener(XMPPEvents.ROOM_CONNECT_ERROR, function (pres) { UI.messageHandler.openReportDialog(null, "dialog.connectError", pres); }); - APP.xmpp.addListener(XMPPEvents.READY_TO_JOIN, function() { + 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); @@ -332,10 +331,14 @@ function registerListeners() { AudioLevels.init(); }); - // 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); + 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); + } } @@ -385,9 +388,6 @@ UI.start = function (init) { $("#welcome_page").hide(); - $("#videospace").mousemove(function () { - return ToolbarToggler.showToolbar(); - }); // Set the defaults for prompt dialogs. $.prompt.setDefaults({persistent: false}); @@ -399,34 +399,39 @@ UI.start = function (init) { bindEvents(); setupPrezi(); - setupToolbars(); - setupChat(); - + 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; - $("#downloadlog").click(function (event) { - dump(event.target); - }); - 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(); - - // Display notice message at the top of the toolbar - if (config.noticeMessage) { - $('#noticeText').text(config.noticeMessage); - $('#notice').css({display: 'block'}); - } if(config.requireDisplayName) { var currentSettings = Settings.getSettings(); @@ -437,30 +442,33 @@ UI.start = function (init) { init(); - 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 - }; + 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(); + + SettingsMenu.init(); + } }; @@ -533,6 +541,8 @@ function onLocalRoleChanged(jid, info, pres, isModerator) { Authentication.closeAuthenticationWindow(); messageHandler.notify(null, "notify.me", 'connected', "notify.moderator"); + + Toolbar.checkAutoRecord(); } } @@ -667,8 +677,8 @@ UI.inputDisplayNameHandler = function (value) { VideoLayout.inputDisplayNameHandler(value); }; -UI.getLargeVideoJid = function() { - return VideoLayout.getLargeVideoJid(); +UI.getLargeVideoResource = function () { + return VideoLayout.getLargeVideoResource(); }; UI.generateRoomName = function() { diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 3a8a0bae6..3fb01b8d4 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -13,6 +13,7 @@ var AuthenticationEvents var roomUrl = null; var sharedKey = ''; var UI = null; +var recordingToaster = null; var buttonHandlers = { "toolbar_button_mute": function () { @@ -122,8 +123,13 @@ function hangup() { * Starts or stops the recording for the conference. */ -function toggleRecording() { +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"); @@ -147,7 +153,7 @@ function toggleRecording() { function () { }, ':input:first' ); - }, Toolbar.setRecordingButtonState, Toolbar.setRecordingButtonState); + }, Toolbar.setRecordingButtonState); } /** @@ -548,17 +554,54 @@ var Toolbar = (function (my) { }; // Sets the state of the recording button - my.setRecordingButtonState = function (isRecording) { + my.setRecordingButtonState = function (recordingState) { var selector = $('#toolbar_button_record'); - if (isRecording) { + + if (recordingState === 'on') { selector.removeClass("icon-recEnable"); selector.addClass("icon-recEnable active"); - } else { + + $("#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) { diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js index fc6bae359..4a7df19fe 100644 --- a/modules/UI/toolbars/ToolbarToggler.js +++ b/modules/UI/toolbars/ToolbarToggler.js @@ -53,6 +53,8 @@ var ToolbarToggler = { * Shows the main toolbar. */ showToolbar: function () { + if (interfaceConfig.filmStripOnly) + return; var header = $("#header"), bottomToolbar = $("#bottomToolbar"); if (!header.is(':visible') || !bottomToolbar.is(":visible")) { @@ -88,6 +90,9 @@ var ToolbarToggler = { * @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')) { diff --git a/modules/UI/util/JitsiPopover.js b/modules/UI/util/JitsiPopover.js index d1dcdbbb8..a66e62822 100644 --- a/modules/UI/util/JitsiPopover.js +++ b/modules/UI/util/JitsiPopover.js @@ -46,6 +46,8 @@ var JitsiPopover = (function () { * Shows the popover */ JitsiPopover.prototype.show = function () { + if(!JitsiPopover.enabled) + return; this.createPopover(); this.popoverShown = true; }; @@ -118,6 +120,8 @@ var JitsiPopover = (function () { this.createPopover(); }; + JitsiPopover.enabled = true; + return JitsiPopover; })(); diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index bec2b60b6..e8f8142e4 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -1,4 +1,11 @@ /* global $, APP, jQuery, toastr */ + +/** + * Flag for enable/disable of the notifications. + * @type {boolean} + */ +var notificationsEnabled = true; + var messageHandler = (function(my) { /** @@ -172,8 +179,19 @@ var messageHandler = (function(my) { 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 = '" + APP.translation.translateString(displayNameKey); } displayNameSpan += ""; - toastr.info( + return toastr.info( displayNameSpan + '
' + '', 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 || {})); diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index a207ae2e0..804d5fb07 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -469,6 +469,14 @@ var LargeVideo = { 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; @@ -481,18 +489,8 @@ var LargeVideo = { var top = availableHeight / 2 - avatarSize / 4 * 3; $('#activeSpeaker').css('top', top); + this.VideoLayout.resizeVideoSpace(animate, isVisible, completeFunction); if(animate) { - $('#videospace').animate({ - right: window.innerWidth - availableWidth, - width: availableWidth, - height: availableHeight - }, - { - queue: false, - duration: 500, - complete: completeFunction - }); - $('#largeVideoContainer').animate({ width: availableWidth, height: availableHeight @@ -502,8 +500,6 @@ var LargeVideo = { duration: 500 }); } else { - $('#videospace').width(availableWidth); - $('#videospace').height(availableHeight); $('#largeVideoContainer').width(availableWidth); $('#largeVideoContainer').height(availableHeight); } diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index e231c1551..0eee1f426 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -42,85 +42,96 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() { * @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 */ -RemoteVideo.prototype.addRemoteVideoMenu = function () { - var spanElement = document.createElement('span'); - spanElement.className = 'remotevideomenu'; - this.container.appendChild(spanElement); +if (!interfaceConfig.filmStripOnly) { + RemoteVideo.prototype.addRemoteVideoMenu = function () { + var spanElement = document.createElement('span'); + spanElement.className = 'remotevideomenu'; - var menuElement = document.createElement('i'); - menuElement.className = 'fa fa-angle-down'; - menuElement.title = 'Remote user controls'; - spanElement.appendChild(menuElement); + 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 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 muteMenuItem = document.createElement('li'); + var muteLinkItem = document.createElement('a'); - var mutedIndicator = ""; + 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'; + if (!this.isMuted) { + muteLinkItem.innerHTML = mutedIndicator + + "
"; + muteLinkItem.className = 'mutelink'; } else { - this.innerHTML = mutedIndicator + - "
"; - this.className = 'mutelink'; + 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")); }; - 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 diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 39cb85f0a..bac1bd2e2 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -3,6 +3,7 @@ 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'); @@ -11,6 +12,7 @@ var RemoteVideo = require("./RemoteVideo"); var LargeVideo = require("./LargeVideo"); var LocalVideo = require("./LocalVideo"); + var remoteVideos = {}; var remoteVideoTypes = {}; var localVideoThumbnail = null; @@ -34,8 +36,12 @@ var VideoLayout = (function (my) { my.init = function (emitter) { eventEmitter = emitter; localVideoThumbnail = new LocalVideo(VideoLayout); + if (interfaceConfig.filmStripOnly) { + LargeVideo.disable(); + } else { + LargeVideo.init(VideoLayout, emitter); + } - LargeVideo.init(VideoLayout, emitter); VideoLayout.resizeLargeVideoContainer(); }; @@ -164,7 +170,7 @@ var VideoLayout = (function (my) { } }; - my.getLargeVideoJid = function () { + my.getLargeVideoResource = function () { return LargeVideo.getResourceJid(); }; @@ -192,7 +198,7 @@ var VideoLayout = (function (my) { resourceJid) { if(focusedVideoResourceJid) { var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid); - if(oldSmallVideo) + if (oldSmallVideo && !interfaceConfig.filmStripOnly) oldSmallVideo.focus(false); } @@ -219,7 +225,7 @@ var VideoLayout = (function (my) { // Update focused/pinned interface. if (resourceJid) { - if(smallVideo) + if (smallVideo && !interfaceConfig.filmStripOnly) smallVideo.focus(true); if (!noPinnedEndpointChangedEvent) { @@ -354,7 +360,11 @@ var VideoLayout = (function (my) { * Resizes the large video container. */ my.resizeLargeVideoContainer = function () { - LargeVideo.resize(); + if(LargeVideo.isEnabled()) { + LargeVideo.resize(); + } else { + VideoLayout.resizeVideoSpace(); + } VideoLayout.resizeThumbnails(); LargeVideo.position(); }; @@ -373,7 +383,7 @@ var VideoLayout = (function (my) { if(animate) { $('#remoteVideos').animate({ - height: height + height: height + 2 // adds 2 px because of small video 1px border }, { queue: false, @@ -398,7 +408,7 @@ var VideoLayout = (function (my) { } else { // size videos so that while keeping AR and max height, we have a // nice fit - $('#remoteVideos').height(height); + $('#remoteVideos').height(height + 2);// adds 2 px because of small video 1px border $('#remoteVideos>span').width(width); $('#remoteVideos>span').height(height); @@ -431,7 +441,7 @@ var VideoLayout = (function (my) { var availableWidth = availableWinWidth / numvids; var aspectRatio = 16.0 / 9.0; var maxHeight = Math.min(160, availableHeight); - availableHeight = Math.min(maxHeight, availableWidth / aspectRatio); + availableHeight = Math.min(maxHeight, availableWidth / aspectRatio, window.innerHeight - 18); if (availableHeight < availableWidth / aspectRatio) { availableWidth = Math.floor(availableHeight * aspectRatio); } @@ -886,6 +896,37 @@ var VideoLayout = (function (my) { 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; diff --git a/modules/URLProcessor/URLProcessor.js b/modules/URLProcessor/URLProcessor.js index 38819ba4c..0aee2ee53 100644 --- a/modules/URLProcessor/URLProcessor.js +++ b/modules/URLProcessor/URLProcessor.js @@ -1,4 +1,4 @@ -/* global $, $iq, config */ +/* global $, $iq, config, interfaceConfig */ var params = {}; function getConfigParamsFromUrl() { if(!location.hash) @@ -17,22 +17,31 @@ params = getConfigParamsFromUrl(); var URLProcessor = { setConfigParametersFromUrl: function () { - for(var k in params) - { - if(typeof k !== "string" || k.indexOf("config.") === -1) + for(var key in params) { + if(typeof key !== "string") continue; - var v = params[k]; - var confKey = k.substr(7); - if(config[confKey] && typeof config[confKey] !== typeof v) + 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 " + k + + console.warn("The type of " + key + " is wrong. That parameter won't be updated in config.js."); continue; } - config[confKey] = v; - + confObj[confKey] = value; } } diff --git a/modules/xmpp/JingleSession.js b/modules/xmpp/JingleSession.js index 5b02be580..1b40a9d85 100644 --- a/modules/xmpp/JingleSession.js +++ b/modules/xmpp/JingleSession.js @@ -1,1454 +1,127 @@ -/* jshint -W117 */ -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 VideoSSRCHack = require("./VideoSSRCHack"); - -// Jingle stuff +/* + * 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; - this.initiator = null; - this.responder = null; - this.isInitiator = null; - this.peerjid = null; - this.state = null; - this.localSDP = null; - this.remoteSDP = null; - this.relayedStreams = []; - this.startTime = null; - this.stopTime = null; - this.media_constraints = null; - this.pc_constraints = null; - this.ice_config = {}; - this.drip_container = []; + + /** + * The XMPP service. + */ this.service = service; - this.eventEmitter = eventEmitter; - this.usetrickle = true; - this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718 - this.usedrip = false; // dripping is sending trickle candidates not one-by-one - - this.hadstuncandidate = false; - this.hadturncandidate = false; - this.lasticecandidate = false; - - this.statsinterval = null; - - this.reason = null; - - this.addssrc = []; - this.removessrc = []; - this.pendingop = null; - this.switchstreams = false; - - this.wait = true; - this.localStreamsSSRC = null; - this.ssrcOwners = {}; - this.ssrcVideoTypes = {}; + /** + * The event emitter. + */ 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. + * Whether to use dripping or not. Dripping is sending trickle candidates + * not one-by-one. + * Note: currently we do not support 'false'. */ - 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(); + 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 = {}; } -JingleSession.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(); - } -}; +/** + * 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; -JingleSession.prototype.initiate = function (peerjid, isInitiator) { - var self = this; if (this.state !== null) { console.error('attempt to initiate on session ' + this.sid + - 'in state ' + this.state); + 'in state ' + this.state); return; } - this.isInitiator = isInitiator; this.state = 'pending'; this.initiator = isInitiator ? this.me : peerjid; this.responder = !isInitiator ? this.me : peerjid; this.peerjid = peerjid; - this.hadstuncandidate = false; - this.hadturncandidate = false; - this.lasticecandidate = false; - this.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': - self.startTime = new Date(); - - // 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; - self.stopTime = new Date(); - // 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); - }); + this.doInitialize(); }; -function onIceConnectionStateChange(sid, session) { - switch (session.peerconnection.iceConnectionState) { - case 'checking': - session.timeChecking = (new Date()).getTime(); - session.firstconnect = true; - break; - case 'completed': // on caller side - case 'connected': - if (session.firstconnect) { - session.firstconnect = false; - var metadata = {}; - metadata.setupTime - = (new Date()).getTime() - session.timeChecking; - session.peerconnection.getStats(function (res) { - if(res && res.result) { - res.result().forEach(function (report) { - if (report.type == 'googCandidatePair' && - report.stat('googActiveConnection') == 'true') { - metadata.localCandidateType - = report.stat('googLocalCandidateType'); - metadata.remoteCandidateType - = report.stat('googRemoteCandidateType'); - - // log pair as well so we can get nice pie - // charts - metadata.candidatePair - = report.stat('googLocalCandidateType') + - ';' + - report.stat('googRemoteCandidateType'); - - if (report.stat('googRemoteAddress').indexOf('[') === 0) - { - metadata.ipv6 = true; - } - } - }); - } - }); - } - break; - } -} - -JingleSession.prototype.accept = function () { - 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(); - - VideoSSRCHack.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'; - JingleSession.onJingleError(self.sid, error); - }, - 10000); - }, - function (e) { - console.error('setLocalDescription failed', e); - self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); - } - ); -}; - -JingleSession.prototype.terminate = function (reason) { - this.state = 'ended'; - this.reason = reason; - this.peerconnection.close(); - if (this.statsinterval !== null) { - window.clearInterval(this.statsinterval); - this.statsinterval = null; - } -}; - -JingleSession.prototype.active = function () { - return this.state == 'active'; -}; - -JingleSession.prototype.sendIceCandidate = function (candidate) { - var self = this; - if (candidate && !this.lasticecandidate) { - var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session); - var jcand = SDPUtil.candidateToJingle(candidate.candidate); - if (!(ice && jcand)) { - console.error('failed to get ice && jcand'); - return; - } - ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; - - if (jcand.type === 'srflx') { - this.hadstuncandidate = true; - } else if (jcand.type === 'relay') { - this.hadturncandidate = true; - } - - if (this.usetrickle) { - if (this.usedrip) { - if (this.drip_container.length === 0) { - // start 20ms callout - window.setTimeout(function () { - if (self.drip_container.length === 0) return; - self.sendIceCandidates(self.drip_container); - self.drip_container = []; - }, 20); - - } - this.drip_container.push(candidate); - return; - } else { - self.sendIceCandidate([candidate]); - } - } - } else { - //console.log('sendIceCandidate: last candidate.'); - if (!this.usetrickle) { - //console.log('should send full offer now...'); - //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 self = this; - var sendJingle = function (ssrc) { - if(!ssrc) - ssrc = {}; - self.localSDP.toJingle( - init, - self.initiator == self.me ? 'initiator' : 'responder', - ssrc); - - VideoSSRCHack.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'; - JingleSession.onJingleError(self.sid, error); - }, - 10000); - } - sendJingle(); - } - this.lasticecandidate = true; - console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate); - console.log('Have we encountered any relay candidates? ' + this.hadturncandidate); - - if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') { - $(document).trigger('nostuncandidates.jingle', [this.sid]); - } - } -}; - -JingleSession.prototype.sendIceCandidates = function (candidates) { - console.log('sendIceCandidates', candidates); - var cand = $iq({to: this.peerjid, type: 'set'}) - .c('jingle', {xmlns: 'urn:xmpp:jingle:1', - action: 'transport-info', - initiator: this.initiator, - sid: this.sid}); - for (var mid = 0; mid < this.localSDP.media.length; mid++) { - var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; }); - var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]); - if (cands.length > 0) { - var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session); - ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; - cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder', - name: (cands[0].sdpMid? cands[0].sdpMid : mline.media) - }).c('transport', ice); - for (var i = 0; i < cands.length; i++) { - cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up(); - } - // add fingerprint - if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) { - var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)); - tmp.required = true; - cand.c( - 'fingerprint', - {xmlns: 'urn:xmpp:jingle:apps:dtls:0'}) - .t(tmp.fingerprint); - delete tmp.fingerprint; - cand.attrs(tmp); - cand.up(); - } - cand.up(); // transport - cand.up(); // content - } - } - // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340 - //console.log('was this the last candidate', this.lasticecandidate); - this.connection.sendIQ(cand, - function () { - var ack = {}; - ack.source = 'transportinfo'; - $(document).trigger('ack.jingle', [this.sid, ack]); - }, - function (stanza) { - var error = ($(stanza).find('error').length) ? { - code: $(stanza).find('error').attr('code'), - reason: $(stanza).find('error :first')[0].tagName, - }:{}; - error.source = 'transportinfo'; - JingleSession.onJingleError(this.sid, error); - }, - 10000); -}; - - -JingleSession.prototype.sendOffer = function () { - //console.log('sendOffer...'); - var self = this; - this.peerconnection.createOffer(function (sdp) { - self.createdOffer(sdp); - }, - function (e) { - console.error('createOffer failed', e); - }, - this.media_constraints - ); -}; - -// FIXME createdOffer is never used in jitsi-meet -JingleSession.prototype.createdOffer = function (sdp) { - //console.log('createdOffer', sdp); - var self = this; - this.localSDP = new SDP(sdp.sdp); - //this.localSDP.mangle(); - var sendJingle = function () { - var init = $iq({to: this.peerjid, - type: 'set'}) - .c('jingle', {xmlns: 'urn:xmpp:jingle:1', - action: 'session-initiate', - initiator: this.initiator, - sid: this.sid}); - self.localSDP.toJingle( - init, - this.initiator == this.me ? 'initiator' : 'responder', - this.localStreamsSSRC); - - VideoSSRCHack.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'; - JingleSession.onJingleError(self.sid, error); - }, - 10000); - } - sdp.sdp = this.localSDP.raw; - this.peerconnection.setLocalDescription(sdp, - function () { - if(self.usetrickle) - { - sendJingle(); - } - self.setLocalDescription(); - //console.log('setLocalDescription success'); - }, - function (e) { - console.error('setLocalDescription failed', e); - 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; - } - } -}; - -JingleSession.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; - } - ); - }); - }); -}; - -JingleSession.prototype.getSsrcOwner = function (ssrc) { - return this.ssrcOwners[ssrc]; -}; - -JingleSession.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); - JingleSession.onJingleFatalError(self, e); - } - ); -}; - -JingleSession.prototype.addIceCandidate = function (elem) { - var self = this; - if (this.peerconnection.signalingState == 'closed') { - return; - } - if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') { - console.log('trickle ice candidate arriving before session accept...'); - // create a PRANSWER for setRemoteDescription - if (!this.remoteSDP) { - var cobbled = 'v=0\r\n' + - 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME - 's=-\r\n' + - 't=0 0\r\n'; - // first, take some things from the local description - for (var i = 0; i < this.localSDP.media.length; i++) { - cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n'; - cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n'; - if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) { - cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n'; - } - cobbled += 'a=inactive\r\n'; - } - this.remoteSDP = new SDP(cobbled); - } - // then add things like ice and dtls from remote candidate - elem.each(function () { - for (var i = 0; i < self.remoteSDP.media.length; i++) { - if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) || - self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { - if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) { - var tmp = $(this).find('transport'); - self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n'; - self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n'; - tmp = $(this).find('transport>fingerprint'); - if (tmp.length) { - self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n'; - } else { - console.log('no dtls fingerprint (webrtc issue #1718?)'); - self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n'; - } - break; - } - } - } - }); - this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join(''); - - // we need a complete SDP with ice-ufrag/ice-pwd in all parts - // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts - // but it could be in the session part as well. since the code above constructs this sdp this can't happen however - var iscomplete = this.remoteSDP.media.filter(function (mediapart) { - return SDPUtil.find_line(mediapart, 'a=ice-ufrag:'); - }).length == this.remoteSDP.media.length; - - if (iscomplete) { - console.log('setting pranswer'); - try { - this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }), - function() { - }, - function(e) { - console.log('setRemoteDescription pranswer failed', e.toString()); - }); - } catch (e) { - console.error('setting pranswer failed', e); - } - } else { - //console.log('not yet setting pranswer'); - } - } - // operate on each content element - elem.each(function () { - // would love to deactivate this, but firefox still requires it - var idx = -1; - var i; - for (i = 0; i < self.remoteSDP.media.length; i++) { - if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) || - self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { - idx = i; - break; - } - } - if (idx == -1) { // fall back to localdescription - for (i = 0; i < self.localSDP.media.length; i++) { - if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) || - self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { - idx = i; - break; - } - } - } - var name = $(this).attr('name'); - // TODO: check ice-pwd and ice-ufrag? - $(this).find('transport>candidate').each(function () { - var line, candidate; - line = SDPUtil.candidateFromJingle(this); - candidate = new RTCIceCandidate({sdpMLineIndex: idx, - sdpMid: name, - candidate: line}); - try { - self.peerconnection.addIceCandidate(candidate); - } catch (e) { - console.error('addIceCandidate failed', e.toString(), line); - } - }); - }); -}; - -JingleSession.prototype.sendAnswer = function (provisional) { - //console.log('createAnswer', provisional); - var self = this; - this.peerconnection.createAnswer( - function (sdp) { - self.createdAnswer(sdp, provisional); - }, - function (e) { - console.error('createAnswer failed', e); - self.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); - }, - this.media_constraints - ); -}; - -JingleSession.prototype.createdAnswer = function (sdp, provisional) { - //console.log('createAnswer callback'); - var self = this; - this.localSDP = new SDP(sdp.sdp); - //this.localSDP.mangle(); - this.usepranswer = provisional === true; - if (this.usetrickle) { - if (this.usepranswer) { - sdp.type = 'pranswer'; - for (var i = 0; i < this.localSDP.media.length; i++) { - this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n'); - } - this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join(''); - } - } - var self = this; - var sendJingle = function (ssrcs) { - // 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); - - VideoSSRCHack.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'; - JingleSession.onJingleError(self.sid, error); - }, - 10000); - } - sdp.sdp = this.localSDP.raw; - this.peerconnection.setLocalDescription(sdp, - function () { - - //console.log('setLocalDescription success'); - if (self.usetrickle && !self.usepranswer) { - sendJingle(); - } - self.setLocalDescription(); - }, - function (e) { - console.error('setLocalDescription failed', e); - 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; - } - } -}; - -JingleSession.prototype.sendTerminate = function (reason, text) { - var self = this, - term = $iq({to: this.peerjid, - type: 'set'}) - .c('jingle', {xmlns: 'urn:xmpp:jingle:1', - action: 'session-terminate', - initiator: this.initiator, - sid: this.sid}) - .c('reason') - .c(reason || 'success'); - - if (text) { - term.up().c('text').t(text); - } - - this.connection.sendIQ(term, - function () { - self.peerconnection.close(); - self.peerconnection = null; - self.terminate(); - var ack = {}; - ack.source = 'terminate'; - $(document).trigger('ack.jingle', [self.sid, ack]); - }, - function (stanza) { - var error = ($(stanza).find('error').length) ? { - code: $(stanza).find('error').attr('code'), - reason: $(stanza).find('error :first')[0].tagName, - }:{}; - $(document).trigger('ack.jingle', [self.sid, error]); - }, - 10000); - if (this.statsinterval !== null) { - window.clearInterval(this.statsinterval); - this.statsinterval = null; - } -}; - -JingleSession.prototype.addSource = function (elem, fromJid) { - - var self = this; - // FIXME: dirty waiting - if (!this.peerconnection.localDescription) - { - console.warn("addSource - localDescription not ready yet") - setTimeout(function() - { - self.addSource(elem, fromJid); - }, - 200 - ); - return; - } - - console.log('addssrc', new Date().getTime()); - console.log('ice', this.peerconnection.iceConnectionState); - - 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); - }); -}; - -JingleSession.prototype.removeSource = function (elem, fromJid) { - - var self = this; - // FIXME: dirty waiting - if (!this.peerconnection.localDescription) - { - console.warn("removeSource - localDescription not ready yet") - setTimeout(function() - { - self.removeSource(elem, fromJid); - }, - 200 - ); - return; - } - - console.log('removessrc', new Date().getTime()); - console.log('ice', this.peerconnection.iceConnectionState); - var sdp = new SDP(this.peerconnection.remoteDescription.sdp); - var mySdp = new SDP(this.peerconnection.localDescription.sdp); - - $(elem).each(function (idx, content) { - var name = $(content).attr('name'); - var lines = ''; - $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() { - var semantics = this.getAttribute('semantics'); - var ssrcs = $(this).find('>source').map(function () { - return this.getAttribute('ssrc'); - }).get(); - - if (ssrcs.length != 0) { - lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; - } - }); - var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source - tmp.each(function () { - var ssrc = $(this).attr('ssrc'); - // This should never happen, but can be useful for bug detection - if(mySdp.containsSSRC(ssrc)){ - console.error("Got remove stream request for my own ssrc: "+ssrc); - return; - } - $(this).find('>parameter').each(function () { - lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); - if ($(this).attr('value') && $(this).attr('value').length) - lines += ':' + $(this).attr('value'); - lines += '\r\n'; - }); - }); - sdp.media.forEach(function(media, idx) { - if (!SDPUtil.find_line(media, 'a=mid:' + name)) - return; - sdp.media[idx] += lines; - if (!self.removessrc[idx]) self.removessrc[idx] = ''; - self.removessrc[idx] += lines; - }); - sdp.raw = sdp.session + sdp.media.join(''); - }); - - this.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); - }); -}; - -JingleSession.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. + * Finishes initialization. */ -JingleSession.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); - }); -}; +JingleSession.prototype.doInitialize = function() {}; /** - * 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. + * Adds the ICE candidates found in the 'contents' array as remote candidates? + * Note: currently only used on transport-info */ -JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { - - if (!(this.peerconnection.signalingState == 'stable' && - this.peerconnection.iceConnectionState == 'connected')){ - console.log("Too early to send updates"); - return; - } - - // send source-remove IQ. - sdpDiffer = new SDPDiffer(new_sdp, old_sdp); - var remove = $iq({to: this.peerjid, type: 'set'}) - .c('jingle', { - xmlns: 'urn:xmpp:jingle:1', - action: 'source-remove', - initiator: this.initiator, - sid: this.sid - } - ); - var removed = sdpDiffer.toJingle(remove); - - // Let 'source-remove' IQ through the hack and see if we're allowed to send - // it in the current form - if (removed) - remove = VideoSSRCHack.processSourceRemove(remove); - - if (removed && 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 = VideoSSRCHack.processSourceAdd(add); - - if (added & 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'); - } -}; +JingleSession.prototype.addIceCandidates = function(contents) {}; /** - * Mutes/unmutes the (local) video i.e. enables/disables all video tracks. + * Handles an 'add-source' event. * - * @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) + * @param contents an array of Jingle 'content' elements. */ -JingleSession.prototype.setVideoMute = function (mute, callback, options) { - var byUser; +JingleSession.prototype.addSources = function(contents) {}; - 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; - } +/** + * Handles a 'remove-source' event. + * + * @param contents an array of Jingle 'content' elements. + */ +JingleSession.prototype.removeSources = function(contents) {}; - this.hardMuteVideo(mute); +/** + * Terminates this Jingle session (stops sending media and closes the streams?) + */ +JingleSession.prototype.terminate = function() {}; - var self = this; - var oldSdp = null; - if(self.peerconnection) { - if(self.peerconnection.localDescription) { - oldSdp = new SDP(self.peerconnection.localDescription.sdp); - } - } +/** + * Sends a Jingle session-terminate message to the peer and terminates the + * session. + * @param reason + * @param text + */ +JingleSession.prototype.sendTerminate = function(reason, text) {}; - this.modifySourcesQueue.push(function() { - console.log('modify sources done'); +/** + * Handles an offer from the remote peer (prepares to accept a session). + * @param jingle the 'jingle' XML element. + */ +JingleSession.prototype.setOffer = function(jingle) {}; - callback(mute); +/** + * Handles an answer from the remote peer (prepares to accept a session). + * @param jingle the 'jingle' XML element. + */ +JingleSession.prototype.setAnswer = function(jingle) {}; - var newSdp = new SDP(self.peerconnection.localDescription.sdp); - console.log("SDPs", oldSdp, newSdp); - self.notifyMySSRCUpdate(oldSdp, newSdp); - }); -}; - -JingleSession.prototype.hardMuteVideo = function (muted) { - this.pendingop = muted ? 'mute' : 'unmute'; -}; - -JingleSession.prototype.sendMute = function (muted, content) { - var info = $iq({to: this.peerjid, - type: 'set'}) - .c('jingle', {xmlns: 'urn:xmpp:jingle:1', - action: 'session-info', - initiator: this.initiator, - sid: this.sid }); - info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'}); - info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'}); - if (content) { - info.attrs({'name': content}); - } - this.connection.send(info); -}; - -JingleSession.prototype.sendRinging = function () { - var info = $iq({to: this.peerjid, - type: 'set'}) - .c('jingle', {xmlns: 'urn:xmpp:jingle:1', - action: 'session-info', - initiator: this.initiator, - sid: this.sid }); - info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'}); - this.connection.send(info); -}; - -JingleSession.prototype.getStats = function (interval) { - var self = this; - var recv = {audio: 0, video: 0}; - var lost = {audio: 0, video: 0}; - var lastrecv = {audio: 0, video: 0}; - var lastlost = {audio: 0, video: 0}; - var loss = {audio: 0, video: 0}; - var delta = {audio: 0, video: 0}; - this.statsinterval = window.setInterval(function () { - if (self && self.peerconnection && self.peerconnection.getStats) { - self.peerconnection.getStats(function (stats) { - var results = stats.result(); - // TODO: there are so much statistics you can get from this.. - for (var i = 0; i < results.length; ++i) { - if (results[i].type == 'ssrc') { - var packetsrecv = results[i].stat('packetsReceived'); - var packetslost = results[i].stat('packetsLost'); - if (packetsrecv && packetslost) { - packetsrecv = parseInt(packetsrecv, 10); - packetslost = parseInt(packetslost, 10); - - if (results[i].stat('googFrameRateReceived')) { - lastlost.video = lost.video; - lastrecv.video = recv.video; - recv.video = packetsrecv; - lost.video = packetslost; - } else { - lastlost.audio = lost.audio; - lastrecv.audio = recv.audio; - recv.audio = packetsrecv; - lost.audio = packetslost; - } - } - } - } - delta.audio = recv.audio - lastrecv.audio; - delta.video = recv.video - lastrecv.video; - loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0; - loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0; - $(document).trigger('packetloss.jingle', [self.sid, loss]); - }); - } - }, interval || 3000); - return this.statsinterval; -}; - -JingleSession.onJingleError = function (session, error) -{ - console.error("Jingle error", error); -} - -JingleSession.onJingleFatalError = function (session, error) -{ - this.service.sessionTerminated = true; - this.connection.emuc.doLeave(); - this.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); - this.eventEmitter.emit(XMPPEvents.JINGLE_FATAL_ERROR, session, error); -} - -JingleSession.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 -function sendKeyframe(pc) { - console.log('sendkeyframe', pc.iceConnectionState); - if (pc.iceConnectionState !== 'connected') return; // safe... - var self = this; - 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); - } - ); -} - - -JingleSession.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 - if (isVideo && - data.peerjid && this.peerjid === data.peerjid && - data.stream.getVideoTracks().length === 0 && - APP.RTC.localVideo.getTracks().length > 0) { - window.setTimeout(function () { - sendKeyframe(self.peerconnection); - }, 3000); - } -} module.exports = JingleSession; diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js new file mode 100644 index 000000000..72da5b1c3 --- /dev/null +++ b/modules/xmpp/JingleSessionPC.js @@ -0,0 +1,1444 @@ +/* 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 +function sendKeyframe(pc) { + console.log('sendkeyframe', pc.iceConnectionState); + if (pc.iceConnectionState !== 'connected') return; // safe... + var self = this; + 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 + 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; diff --git a/modules/xmpp/VideoSSRCHack.js b/modules/xmpp/LocalSSRCReplacement.js similarity index 60% rename from modules/xmpp/VideoSSRCHack.js rename to modules/xmpp/LocalSSRCReplacement.js index 9e28d82d9..0ee0459b2 100644 --- a/modules/xmpp/VideoSSRCHack.js +++ b/modules/xmpp/LocalSSRCReplacement.js @@ -1,8 +1,21 @@ /* global $ */ /* - The purpose of this hack is to re-use SSRC of first video stream ever created - for any video streams created later on. In order to do that this hack: + 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 @@ -19,12 +32,32 @@ */ 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. @@ -36,16 +69,17 @@ var localVideoSSRC; * other SSRCs left to be signaled after removing it. */ var filterOutSource = function (modifyIq, actionName) { - if (!localVideoSSRC) - return modifyIq; - 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; + return modifyIqTree[0]; } console.info( @@ -55,7 +89,7 @@ var filterOutSource = function (modifyIq, actionName) { // Check if any sources still left to be added/removed if (modifyIqTree.find('>jingle>content>description>source').length) { - return modifyIqTree; + return modifyIqTree[0]; } else { return null; } @@ -70,21 +104,41 @@ var storeLocalVideoSSRC = function (jingleIq) { $(jingleIq.tree()) .find('>jingle>content[name="video"]>description>source'); - console.info('Video desc: ', videoSSRCs); - if (!videoSSRCs.length) - return; - - var ssrc = videoSSRCs.attr('ssrc'); - if (ssrc) { - localVideoSSRC = ssrc; - console.info( - 'Stored local video SSRC for future re-use: ' + localVideoSSRC); - } else { - console.error('No "ssrc" attribute present in element'); - } + 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); + } + } + }); }; -var LocalVideoSSRCHack = { +/** + * 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. @@ -93,6 +147,9 @@ var LocalVideoSSRCHack = { * which will be scanned for local video SSRC. */ processSessionInit: function (sessionInit) { + if (!isEnabled) + return; + if (localVideoSSRC) { console.error("Local SSRC stored already: " + localVideoSSRC); return; @@ -108,6 +165,9 @@ var LocalVideoSSRCHack = { * @returns modified localDescription object. */ mungeLocalVideoSSRC: function (localDescription) { + if (!isEnabled) + return localDescription; + // IF we have local video SSRC stored make sure it is replaced // with old SSRC if (localVideoSSRC) { @@ -129,6 +189,25 @@ var LocalVideoSSRCHack = { 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; }, @@ -143,6 +222,9 @@ var LocalVideoSSRCHack = { * 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); @@ -162,8 +244,20 @@ var LocalVideoSSRCHack = { * 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 = LocalVideoSSRCHack; +module.exports = LocalSSRCReplacement; diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 1bc68f122..ee748fd39 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -1,7 +1,7 @@ var RTC = require('../RTC/RTC'); var RTCBrowserType = require("../RTC/RTCBrowserType.js"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); -var VideoSSRCHack = require("./VideoSSRCHack"); +var SSRCReplacement = require("./LocalSSRCReplacement"); function TraceablePeerConnection(ice_config, constraints, session) { var self = this; @@ -213,7 +213,7 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; - desc = VideoSSRCHack.mungeLocalVideoSSRC(desc); + desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); @@ -372,7 +372,7 @@ TraceablePeerConnection.prototype.createOffer self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer)); } - offer = VideoSSRCHack.mungeLocalVideoSSRC(offer); + offer = SSRCReplacement.mungeLocalVideoSSRC(offer); if (config.enableSimulcast && self.simulcast.isSupported()) { offer = self.simulcast.mungeLocalDescription(offer); @@ -402,7 +402,7 @@ TraceablePeerConnection.prototype.createAnswer } // munge local video SSRC - answer = VideoSSRCHack.mungeLocalVideoSSRC(answer); + answer = SSRCReplacement.mungeLocalVideoSSRC(answer); if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer); diff --git a/modules/xmpp/recording.js b/modules/xmpp/recording.js index a542e59c3..af986b400 100644 --- a/modules/xmpp/recording.js +++ b/modules/xmpp/recording.js @@ -19,6 +19,12 @@ var useJirecon = (typeof config.hosts.jirecon != "undefined"); */ 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; } @@ -30,9 +36,9 @@ function setRecordingJirecon(state, token, callback, connection) { var iq = $iq({to: config.hosts.jirecon, type: 'set'}) .c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon', - action: state ? 'start' : 'stop', + action: (state === 'on') ? 'start' : 'stop', mucjid: connection.emuc.roomjid}); - if (!state){ + if (state === 'off'){ iq.attrs({rid: jireconRid}); } @@ -44,10 +50,10 @@ function setRecordingJirecon(state, token, callback, connection) { // TODO wait for an IQ with the real status, since this is // provisional? jireconRid = $(result).find('recording').attr('rid'); - console.log('Recording ' + (state ? 'started' : 'stopped') + + console.log('Recording ' + ((state === 'on') ? 'started' : 'stopped') + '(jirecon)' + result); recordingEnabled = state; - if (!state){ + if (state === 'off'){ jireconRid = null; } @@ -73,10 +79,19 @@ function setRecordingColibri(state, token, callback, connection) { function (result) { console.log('Set recording "', state, '". Result:', result); var recordingElem = $(result).find('>conference>recording'); - var newState = ('true' === recordingElem.attr('state')); + 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); @@ -94,8 +109,7 @@ function setRecording(state, token, callback, connection) { } var Recording = { - toggleRecording: function (tokenEmptyCallback, - startingCallback, startedCallback, connection) { + toggleRecording: function (tokenEmptyCallback, recordingStateChangeCallback, connection) { if (!Moderator.isModerator()) { console.log( 'non-focus, or conference not yet organized:' + @@ -108,16 +122,16 @@ var Recording = { if (!recordingToken && !useJirecon) { tokenEmptyCallback(function (value) { setRecordingToken(value); - self.toggleRecording(tokenEmptyCallback, - startingCallback, startedCallback, connection); + self.toggleRecording(tokenEmptyCallback, recordingStateChangeCallback, connection); }); return; } var oldState = recordingEnabled; - startingCallback(!oldState); - setRecording(!oldState, + var newState = (oldState === 'off' || !oldState) ? 'on' : 'off'; + + setRecording(newState, recordingToken, function (state) { console.log("New recording state: ", state); @@ -143,7 +157,7 @@ var Recording = { // have been wrong setRecordingToken(null); } - startedCallback(state); + recordingStateChangeCallback(state); }, connection diff --git a/modules/xmpp/strophe.jingle.js b/modules/xmpp/strophe.jingle.js index f5558aec2..f1adc5f6b 100644 --- a/modules/xmpp/strophe.jingle.js +++ b/modules/xmpp/strophe.jingle.js @@ -1,27 +1,11 @@ /* jshint -W117 */ -var JingleSession = require("./JingleSession"); +var JingleSession = require("./JingleSessionPC"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var RTCBrowserType = require("../RTC/RTCBrowserType"); module.exports = function(XMPP, eventEmitter) { - function CallIncomingJingle(sid, connection) { - var sess = connection.jingle.sessions[sid]; - - // TODO: do we check activecall == null? - connection.jingle.activecall = sess; - - eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess); - - // TODO: check affiliation and/or role - console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); - sess.usedrip = true; // not-so-naive trickle ice - sess.sendAnswer(); - sess.accept(); - - } - Strophe.addConnectionPlugin('jingle', { connection: null, sessions: {}, @@ -126,20 +110,30 @@ module.exports = function(XMPP, eventEmitter) { sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; - sess.initiate(fromJid, false); + sess.initialize(fromJid, false); // FIXME: setRemoteDescription should only be done when this call is to be accepted - sess.setRemoteDescription($(iq).find('>jingle'), 'offer'); + 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 synchronus - CallIncomingJingle(sess.sid, this.connection); + // 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.setRemoteDescription($(iq).find('>jingle'), 'answer'); + sess.setAnswer($(iq).find('>jingle')); sess.accept(); $(document).trigger('callaccepted.jingle', [sess.sid]); break; @@ -206,7 +200,7 @@ module.exports = function(XMPP, eventEmitter) { sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; - sess.initiate(peerjid, true); + sess.initialize(peerjid, true); this.sessions[sess.sid] = sess; this.jid2session[sess.peerjid] = sess; sess.sendOffer(); diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js index 38ab670d2..6ee7db996 100644 --- a/modules/xmpp/xmpp.js +++ b/modules/xmpp/xmpp.js @@ -434,9 +434,9 @@ var XMPP = { return true; }, toggleRecording: function (tokenEmptyCallback, - startingCallback, startedCallback) { + recordingStateChangeCallback) { Recording.toggleRecording(tokenEmptyCallback, - startingCallback, startedCallback, connection); + recordingStateChangeCallback, connection); }, addToPresence: function (name, value, dontSend) { switch (name) {