Merge pull request #1775 from jitsi/start_screen

Add config.startScreenSharing
This commit is contained in:
Saúl Ibarra Corretgé 2017-07-18 13:51:57 +02:00 committed by GitHub
commit c97daff506
5 changed files with 356 additions and 198 deletions

View File

@ -72,11 +72,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";
/*
@ -118,68 +113,6 @@ function connect(roomName) {
});
}
/**
* Creates local media tracks and connects to room. Will show error
* dialogs in case if accessing local microphone and/or camera failed. Will
* show guidance overlay for users on how to give access to camera and/or
* microphone,
* @param {string} roomName
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
*/
function createInitialLocalTracksAndConnect(roomName) {
let audioAndVideoError,
audioOnlyError,
videoOnlyError;
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMeetJS.events.mediaDevices.PERMISSION_PROMPT_IS_SHOWN,
browser =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browser))
);
// 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;
// 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 both requests for 'audio' + 'video' and 'audio' only
// failed, we assume that there is some problems with user's
// microphone and show corresponding dialog.
APP.UI.showDeviceErrorDialog(
audioOnlyError, videoOnlyError);
} else {
// If request for 'audio' + 'video' failed, but request for
// 'audio' only was OK, we assume that we had problems with
// camera and show corresponding dialog.
APP.UI.showDeviceErrorDialog(null, audioAndVideoError);
}
}
return [tracks, con];
});
}
/**
* Share data to other users.
* @param command the command
@ -567,6 +500,106 @@ export default {
* Whether the local participant is the dominant speaker in the conference.
*/
isDominantSpeaker: false,
/**
* Creates local media tracks and connects to a room. Will show error
* dialogs in case accessing the local microphone and/or camera failed. Will
* show guidance overlay for users on how to give access to camera and/or
* microphone,
* @param {string} roomName
* @param {object} options
* @param {boolean} options.startScreenSharing - if <tt>true</tt> should
* start with screensharing instead of camera video.
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
*/
createInitialLocalTracksAndConnect(roomName, options = {}) {
let audioAndVideoError,
audioOnlyError,
screenSharingError,
videoOnlyError;
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMeetJS.events.mediaDevices.PERMISSION_PROMPT_IS_SHOWN,
browser =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browser))
);
// First try to retrieve both audio and video.
let tryCreateLocalTracks;
// FIXME the logic about trying to go audio only on error is duplicated
if (options.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;
// 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));
// FIXME If there will be microphone error it will cover any
// screensharing dialog, but it's still better than in
// the reverse order where the screensharing dialog will
// sometimes be closing the microphone alert ($.prompt.close();
// is called). Need to figure out dialogs chaining to fix that.
if (screenSharingError) {
this._handleScreenSharingError(screenSharingError);
}
if (audioAndVideoError || audioOnlyError) {
if (audioOnlyError || videoOnlyError) {
// If both requests for 'audio' + 'video' and 'audio'
// only failed, we assume that there are some problems
// with user's microphone and show corresponding dialog.
APP.UI.showDeviceErrorDialog(
audioOnlyError, videoOnlyError);
} else {
// If request for 'audio' + 'video' failed, but request
// for 'audio' only was OK, we assume that we had
// problems with camera and show corresponding dialog.
APP.UI.showDeviceErrorDialog(null, audioAndVideoError);
}
}
return [tracks, con];
});
},
/**
* Open new connection and join to the conference.
* @param {object} options
@ -602,7 +635,10 @@ export default {
{enableAnalyticsLogging: analytics.isEnabled()}, config)
).then(() => {
analytics.init();
return createInitialLocalTracksAndConnect(options.roomName);
return this.createInitialLocalTracksAndConnect(
options.roomName, {
startScreenSharing: config.startScreenSharing
});
}).then(([tracks, con]) => {
tracks.forEach(track => {
if((track.isAudioTrack() && initialAudioMutedState)
@ -1215,7 +1251,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<string>} [options.desktopSharingSources] - Array with the
@ -1239,8 +1274,26 @@ export default {
}
if (!this._untoggleScreenSharing) {
this.videoSwitchInProgress = true;
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.<JitsiLocalTrack>} - 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;
@ -1254,32 +1307,45 @@ export default {
},
listener: (status, url) => {
switch(status) {
case "waitingForExtension":
case "waitingForExtension": {
DSExternalInstallationInProgress = true;
externalInstallation = true;
APP.UI.showExtensionExternalInstallationDialog(
url);
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
}
case "extensionFound": {
if (externalInstallation) { //close the dialog
$.prompt.close();
}
break;
default:
}
default: {
//Unknown status
}
}
}
}).then(([stream]) => {
}
}).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);
DSExternalInstallationInProgress = false;
// close external installation dialog on success.
if(externalInstallation)
$.prompt.close();
stream.on(
desktopStream.on(
TrackEvents.LOCAL_TRACK_STOPPED,
() => {
// If the stream was stopped during screen sharing
@ -1290,22 +1356,48 @@ export default {
}
}
);
// 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');
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();
}).catch(error => {
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
@ -1318,13 +1410,49 @@ export default {
this._untoggleScreenSharing();
}
logger.error('failed to share local desktop', err);
// 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);
});
},
if (err.name === TrackErrors.FIREFOX_EXTENSION_NEEDED) {
/**
* 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 Promise.reject(err);
return;
}
// Handling:
@ -1335,7 +1463,7 @@ export default {
let dialogTxt;
let dialogTitleKey;
if (err.name === TrackErrors.PERMISSION_DENIED) {
if (error.name === TrackErrors.PERMISSION_DENIED) {
dialogTxt = APP.translation.generateTranslationHTML(
"dialog.screenSharingPermissionDeniedError");
dialogTitleKey = "dialog.error";
@ -1345,12 +1473,7 @@ export default {
dialogTitleKey = "dialog.permissionDenied";
}
APP.UI.messageHandler.openDialog(
dialogTitleKey, dialogTxt, false);
});
} else {
return this._untoggleScreenSharing();
}
APP.UI.messageHandler.openDialog(dialogTitleKey, dialogTxt, false);
},
/**
* Setup interaction between conference and UI.
@ -1637,17 +1760,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) {

View File

@ -72,6 +72,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",

View File

@ -333,6 +333,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",

View File

@ -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 () {
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.

View File

@ -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.