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 = '" + displayName;
@@ -7961,7 +8054,7 @@ var messageHandler = (function(my) {
"'>" + 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 = '" + displayName;
@@ -182,7 +200,7 @@ var messageHandler = (function(my) {
"'>" + 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) {