diff --git a/conference.js b/conference.js
index 9f1a42fdc..b4fdebeef 100644
--- a/conference.js
+++ b/conference.js
@@ -78,11 +78,6 @@ let connection;
let localAudio, localVideo;
let initialAudioMutedState = false, initialVideoMutedState = false;
-/**
- * Indicates whether extension external installation is in progress or not.
- */
-let DSExternalInstallationInProgress = false;
-
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
/*
@@ -498,11 +493,14 @@ export default {
* show guidance overlay for users on how to give access to camera and/or
* microphone,
* @param {string} roomName
+ * @param {boolean} startScreenSharing - if true should start with
+ * screensharing instead of camera video.
* @returns {Promise.}
*/
- createInitialLocalTracksAndConnect(roomName) {
+ createInitialLocalTracksAndConnect(roomName, startScreenSharing) {
let audioAndVideoError,
audioOnlyError,
+ screenSharingError,
videoOnlyError;
JitsiMeetJS.mediaDevices.addEventListener(
@@ -513,31 +511,55 @@ export default {
);
// First try to retrieve both audio and video.
- let tryCreateLocalTracks = createLocalTracks(
- { devices: ['audio', 'video'] }, true)
- .catch(err => {
- // If failed then try to retrieve only audio.
- audioAndVideoError = err;
- return createLocalTracks({ devices: ['audio'] }, true);
- })
- .catch(err => {
- audioOnlyError = err;
+ let tryCreateLocalTracks;
- // Try video only...
- return createLocalTracks({ devices: ['video'] }, true);
- })
- .catch(err => {
- videoOnlyError = err;
+ // FIXME the logic about trying to go audio only on error is duplicated
+ if (startScreenSharing) {
+ tryCreateLocalTracks = this._createDesktopTrack()
+ .then(desktopStream => {
+ return createLocalTracks({ devices: ['audio'] }, true)
+ .then(([audioStream]) => {
+ return [desktopStream, audioStream];
+ })
+ .catch(error => {
+ audioOnlyError = error;
+ return [desktopStream];
+ });
+ }).catch(error => {
+ logger.error('Failed to obtain desktop stream', error);
+ screenSharingError = error;
+ return createLocalTracks({ devices: ['audio'] }, true);
+ }).catch(error => {
+ audioOnlyError = error;
+ return [];
+ });
+ } else {
+ tryCreateLocalTracks = createLocalTracks(
+ {devices: ['audio', 'video']}, true)
+ .catch(err => {
+ // If failed then try to retrieve only audio.
+ audioAndVideoError = err;
+ return createLocalTracks({devices: ['audio']}, true);
+ })
+ .catch(err => {
+ audioOnlyError = err;
- return [];
- });
+ // Try video only...
+ return createLocalTracks({devices: ['video']}, true);
+ })
+ .catch(err => {
+ videoOnlyError = err;
+
+ return [];
+ });
+ }
return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
.then(([tracks, con]) => {
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(false));
- if (audioAndVideoError) {
- if (audioOnlyError) {
+ if (audioAndVideoError || audioOnlyError) {
+ if (audioOnlyError || videoOnlyError) {
// If both requests for 'audio' + 'video' and 'audio'
// only failed, we assume that there is some problems
// with user's microphone and show corresponding dialog.
@@ -551,6 +573,18 @@ export default {
}
}
+ // FIXME If there was a screen sharing error or the extension
+ // needs to be installed it will appear on top of eventual
+ // "microphone error" dialog. That is not great, but currently
+ // it's pretty hard to chain dialogs since they don't return
+ // Promises.
+ if (screenSharingError) {
+ // FIXME if _handleScreenSharingError will be dealing with
+ // installing external extension it may close previously
+ // opened microphone dialog ($.prompt.close(); is called).
+ this._handleScreenSharingError(screenSharingError);
+ }
+
return [tracks, con];
});
},
@@ -591,7 +625,8 @@ export default {
).then(() => {
analytics.init();
return this.createInitialLocalTracksAndConnect(
- options.roomName);
+ options.roomName,
+ config.startScreenSharing);
}).then(([tracks, con]) => {
tracks.forEach(track => {
if((track.isAudioTrack() && initialAudioMutedState)
@@ -1197,7 +1232,6 @@ export default {
/**
* Toggles between screensharing and camera video.
- * @param {boolean} [shareScreen]
* @param {Object} [options] - Screen sharing options that will be passed to
* createLocalTracks.
* @param {Array} [options.desktopSharingSources] - Array with the
@@ -1221,119 +1255,206 @@ export default {
}
if (!this._untoggleScreenSharing) {
- this.videoSwitchInProgress = true;
- let externalInstallation = false;
- const didHaveVideo = Boolean(localVideo);
- const wasVideoMuted = this.videoMuted;
-
- return createLocalTracks({
- desktopSharingSources: options.desktopSharingSources,
- devices: ['desktop'],
- desktopSharingExtensionExternalInstallation: {
- interval: 500,
- checkAgain: () => {
- return DSExternalInstallationInProgress;
- },
- listener: (status, url) => {
- switch(status) {
- case "waitingForExtension":
- DSExternalInstallationInProgress = true;
- externalInstallation = true;
- APP.UI.showExtensionExternalInstallationDialog(
- url);
- break;
- case "extensionFound":
- if(externalInstallation) //close the dialog
- $.prompt.close();
- break;
- default:
- //Unknown status
- }
- }
- }
- }).then(([stream]) => {
- // Stores the "untoggle" handler which remembers whether was
- // there any video before and whether was it muted.
- this._untoggleScreenSharing
- = this._turnScreenSharingOff
- .bind(this, didHaveVideo, wasVideoMuted);
- DSExternalInstallationInProgress = false;
- // close external installation dialog on success.
- if(externalInstallation)
- $.prompt.close();
- stream.on(
- TrackEvents.LOCAL_TRACK_STOPPED,
- () => {
- // If the stream was stopped during screen sharing
- // session then we should switch back to video.
- if (this.isSharingScreen){
- this._untoggleScreenSharing
- && this._untoggleScreenSharing();
- }
- }
- );
- return this.useVideoStream(stream);
- }).then(() => {
- this.videoSwitchInProgress = false;
- JitsiMeetJS.analytics.sendEvent(
- 'conference.sharingDesktop.start');
- logger.log('sharing local desktop');
- }).catch(err => {
- // close external installation dialog to show the error.
- if(externalInstallation)
- $.prompt.close();
- this.videoSwitchInProgress = false;
-
- if (err.name === TrackErrors.CHROME_EXTENSION_USER_CANCELED) {
- return Promise.reject(err);
- }
-
- // Pawel: With this call I'm trying to preserve the original
- // behaviour although it is not clear why would we "untoggle"
- // on failure. I suppose it was to restore video in case there
- // was some problem during "this.useVideoStream(desktopStream)".
- // It's important to note that the handler will not be available
- // if we fail early on trying to get desktop media (which makes
- // sense, because the camera video is still being used, so
- // nothing to "untoggle").
- if (this._untoggleScreenSharing) {
- this._untoggleScreenSharing();
- }
-
- logger.error('failed to share local desktop', err);
-
- if (err.name === TrackErrors.FIREFOX_EXTENSION_NEEDED) {
- APP.UI.showExtensionRequiredDialog(
- config.desktopSharingFirefoxExtensionURL
- );
- return Promise.reject(err);
- }
-
- // Handling:
- // TrackErrors.PERMISSION_DENIED
- // TrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR
- // TrackErrors.GENERAL
- // and any other
- let dialogTxt;
- let dialogTitleKey;
-
- if (err.name === TrackErrors.PERMISSION_DENIED) {
- dialogTxt = APP.translation.generateTranslationHTML(
- "dialog.screenSharingPermissionDeniedError");
- dialogTitleKey = "dialog.error";
- } else {
- dialogTxt = APP.translation.generateTranslationHTML(
- "dialog.failtoinstall");
- dialogTitleKey = "dialog.permissionDenied";
- }
-
- APP.UI.messageHandler.openDialog(
- dialogTitleKey, dialogTxt, false);
- });
+ return this._switchToScreenSharing(options);
} else {
return this._untoggleScreenSharing();
}
},
+
+ /**
+ * Creates desktop (screensharing) {@link JitsiLocalTrack}
+ * @param {Object} [options] - Screen sharing options that will be passed to
+ * createLocalTracks.
+ *
+ * @return {Promise.} - A Promise resolved with
+ * {@link JitsiLocalTrack} for the screensharing or rejected with
+ * {@link JitsiTrackError}.
+ *
+ * @private
+ */
+ _createDesktopTrack(options = {}) {
+ let externalInstallation = false;
+ let DSExternalInstallationInProgress = false;
+ const didHaveVideo = Boolean(localVideo);
+ const wasVideoMuted = this.videoMuted;
+
+ return createLocalTracks({
+ desktopSharingSources: options.desktopSharingSources,
+ devices: ['desktop'],
+ desktopSharingExtensionExternalInstallation: {
+ interval: 500,
+ checkAgain: () => {
+ return DSExternalInstallationInProgress;
+ },
+ listener: (status, url) => {
+ switch(status) {
+ case "waitingForExtension": {
+ DSExternalInstallationInProgress = true;
+ externalInstallation = true;
+ const listener = () => {
+ // Wait a little bit more just to be sure that
+ // we won't miss the extension installation
+ setTimeout(
+ () => {
+ DSExternalInstallationInProgress = false;
+ }, 500);
+ APP.UI.removeListener(
+ UIEvents.EXTERNAL_INSTALLATION_CANCELED,
+ listener);
+ };
+ APP.UI.addListener(
+ UIEvents.EXTERNAL_INSTALLATION_CANCELED,
+ listener);
+ APP.UI.showExtensionExternalInstallationDialog(url);
+ break;
+ }
+ case "extensionFound": {
+ if (externalInstallation) //close the dialog
+ $.prompt.close();
+ break;
+ }
+ default: {
+ //Unknown status
+ }
+ }
+ }
+ }
+ }).then(([desktopStream])=> {
+ // Stores the "untoggle" handler which remembers whether was
+ // there any video before and whether was it muted.
+ this._untoggleScreenSharing
+ = this._turnScreenSharingOff
+ .bind(this, didHaveVideo, wasVideoMuted);
+ desktopStream.on(
+ TrackEvents.LOCAL_TRACK_STOPPED,
+ () => {
+ // If the stream was stopped during screen sharing
+ // session then we should switch back to video.
+ if (this.isSharingScreen) {
+ this._untoggleScreenSharing
+ && this._untoggleScreenSharing();
+ }
+ }
+ );
+ // close external installation dialog on success.
+ if (externalInstallation) {
+ $.prompt.close();
+ }
+ return desktopStream;
+ }, error => {
+ DSExternalInstallationInProgress = false;
+ // close external installation dialog on success.
+ if (externalInstallation) {
+ $.prompt.close();
+ }
+ throw error;
+ });
+ },
+
+ /**
+ * Tries to switch to the screenshairng mode by disposing camera stream and
+ * replacing it with a desktop one.
+ *
+ * @param {Object} [options] - Screen sharing options that will be passed to
+ * createLocalTracks.
+ *
+ * @return {Promise} - A Promise resolved if the operation succeeds or
+ * rejected with some unknown type of error in case it fails. Promise will
+ * be rejected immediately if {@link videoSwitchInProgress} is true.
+ *
+ * @private
+ */
+ _switchToScreenSharing(options = {}) {
+ if (this.videoSwitchInProgress) {
+ return Promise.reject('Switch in progress.');
+ }
+
+ this.videoSwitchInProgress = true;
+ return this._createDesktopTrack(options).then(stream => {
+ return this.useVideoStream(stream);
+ }).then(() => {
+ this.videoSwitchInProgress = false;
+ JitsiMeetJS.analytics.sendEvent('conference.sharingDesktop.start');
+ logger.log('sharing local desktop');
+ }).catch(error => {
+ this.videoSwitchInProgress = false;
+ // Pawel: With this call I'm trying to preserve the original
+ // behaviour although it is not clear why would we "untoggle"
+ // on failure. I suppose it was to restore video in case there
+ // was some problem during "this.useVideoStream(desktopStream)".
+ // It's important to note that the handler will not be available
+ // if we fail early on trying to get desktop media (which makes
+ // sense, because the camera video is still being used, so
+ // nothing to "untoggle").
+ if (this._untoggleScreenSharing) {
+ this._untoggleScreenSharing();
+ }
+
+ // FIXME the code inside of _handleScreenSharingError is
+ // asynchronous, but does not return a Promise and is not part of
+ // the current Promise chain.
+ this._handleScreenSharingError(error);
+ });
+ },
+
+ /**
+ * Handles {@link JitsiTrackError} returned by the lib-jitsi-meet when
+ * trying to create screensharing track. It will either do nothing if
+ * the dialog was canceled on user's request or display inline installation
+ * dialog and ask the user to install the extension, once the extension is
+ * installed it will switch the conference to screensharing. The last option
+ * is that an unrecoverable error dialog will be displayed.
+ * @param {JitsiTrackError} error - The error returned by
+ * {@link _createDesktopTrack} Promise.
+ * @private
+ */
+ _handleScreenSharingError(error) {
+ if (error.name === TrackErrors.CHROME_EXTENSION_USER_CANCELED) {
+ return;
+ }
+
+ logger.error('failed to share local desktop', error);
+
+ if (error.name === TrackErrors.CHROME_EXTENSION_USER_GESTURE_REQUIRED) {
+ // If start with screen sharing the extension will fail to install
+ // (if not found), because the request has been triggered by the
+ // script. Show a dialog which asks user to click "install" and try
+ // again switching to the screen sharing.
+ APP.UI.showExtensionInlineInstallationDialog(
+ () => {
+ this.toggleScreenSharing();
+ }
+ );
+
+ return;
+ } else if (error.name === TrackErrors.FIREFOX_EXTENSION_NEEDED) {
+ APP.UI.showExtensionRequiredDialog(
+ config.desktopSharingFirefoxExtensionURL
+ );
+
+ return;
+ }
+
+ // Handling:
+ // TrackErrors.PERMISSION_DENIED
+ // TrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR
+ // TrackErrors.GENERAL
+ // and any other
+ let dialogTxt;
+ let dialogTitleKey;
+
+ if (error.name === TrackErrors.PERMISSION_DENIED) {
+ dialogTxt = APP.translation.generateTranslationHTML(
+ "dialog.screenSharingPermissionDeniedError");
+ dialogTitleKey = "dialog.error";
+ } else {
+ dialogTxt = APP.translation.generateTranslationHTML(
+ "dialog.failtoinstall");
+ dialogTitleKey = "dialog.permissionDenied";
+ }
+
+ APP.UI.messageHandler.openDialog(dialogTitleKey, dialogTxt, false);
+ },
/**
* Setup interaction between conference and UI.
*/
@@ -1619,17 +1740,6 @@ export default {
APP.UI.updateDTMFSupport(isDTMFSupported);
});
- APP.UI.addListener(UIEvents.EXTERNAL_INSTALLATION_CANCELED, () => {
- // Wait a little bit more just to be sure that we won't miss the
- // extension installation
- setTimeout(() => DSExternalInstallationInProgress = false, 500);
- });
- APP.UI.addListener(UIEvents.OPEN_EXTENSION_STORE, (url) => {
- window.open(
- url, "extension_store_window",
- "resizable,scrollbars=yes,status=1");
- });
-
APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio);
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
if (this.isAudioOnly() && !muted) {
diff --git a/config.js b/config.js
index 9d1c41367..92046b647 100644
--- a/config.js
+++ b/config.js
@@ -69,6 +69,7 @@ var config = { // eslint-disable-line no-unused-vars
// page redirection when call is hangup
disableSimulcast: false,
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
+ startScreenSharing: false, // Will try to start with screensharing instead of camera
// startAudioMuted: 10, // every participant after the Nth will start audio muted
// startVideoMuted: 10, // every participant after the Nth will start video muted
// defaultLanguage: "en",
diff --git a/lang/main.json b/lang/main.json
index 461854422..fd71fd126 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -332,6 +332,8 @@
"goToStore": "Go to the webstore",
"externalInstallationTitle": "Extension required",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
+ "inlineInstallationMsg": "You need to install our desktop sharing extension.",
+ "inlineInstallExtension": "Install now",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 723b500c0..af89d36a6 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -1158,15 +1158,34 @@ UI.showExtensionRequiredDialog = function (url) {
* @param url {string} the url of the extension.
*/
UI.showExtensionExternalInstallationDialog = function (url) {
+ let openedWindow = null;
+
let submitFunction = function(e,v){
if (v) {
e.preventDefault();
- eventEmitter.emit(UIEvents.OPEN_EXTENSION_STORE, url);
+ if (openedWindow === null || openedWindow.closed) {
+ openedWindow
+ = window.open(
+ url,
+ "extension_store_window",
+ "resizable,scrollbars=yes,status=1");
+ } else {
+ openedWindow.focus();
+ }
}
};
- let closeFunction = function () {
- eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
+ let closeFunction = function (e, v) {
+ if (openedWindow) {
+ // Ideally we would close the popup, but this does not seem to work
+ // on Chrome. Leaving it uncommented in case it could work
+ // in some version.
+ openedWindow.close();
+ openedWindow = null;
+ }
+ if (!v) {
+ eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
+ }
};
messageHandler.openTwoButtonDialog({
@@ -1179,6 +1198,36 @@ UI.showExtensionExternalInstallationDialog = function (url) {
});
};
+/**
+ * Shows a dialog which asks user to install the extension. This one is
+ * displayed after installation is triggered from the script, but fails because
+ * it must be initiated by user gesture.
+ * @param callback {function} function to be executed after user clicks
+ * the install button - it should make another attempt to install the extension.
+ */
+UI.showExtensionInlineInstallationDialog = function (callback) {
+ let submitFunction = function(e,v){
+ if (v) {
+ callback();
+ }
+ };
+
+ let closeFunction = function (e, v) {
+ if (!v) {
+ eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
+ }
+ };
+
+ messageHandler.openTwoButtonDialog({
+ titleKey: 'dialog.externalInstallationTitle',
+ msgKey: 'dialog.inlineInstallationMsg',
+ leftButtonKey: 'dialog.inlineInstallExtension',
+ submitFunction,
+ loadedFunction: $.noop,
+ closeFunction
+ });
+};
+
/**
* Shows dialog with combined information about camera and microphone errors.
diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js
index d4d445120..3f033d1aa 100644
--- a/service/UI/UIEvents.js
+++ b/service/UI/UIEvents.js
@@ -97,12 +97,6 @@ export default {
// changed.
RESOLUTION_CHANGED: "UI.resolution_changed",
- /**
- * Notifies that the button "Go to webstore" is pressed on the dialog for
- * external extension installation.
- */
- OPEN_EXTENSION_STORE: "UI.open_extension_store",
-
/**
* Notifies that the button "Cancel" is pressed on the dialog for
* external extension installation.