feat: add config.startScreenSharing

Will try to use screensharing instead of camera video from
the beginning.
This commit is contained in:
paweldomas 2017-06-29 19:43:35 +02:00
parent 5b5470ec66
commit 3926d705ad
5 changed files with 311 additions and 155 deletions

View File

@ -78,11 +78,6 @@ let connection;
let localAudio, localVideo; let localAudio, localVideo;
let initialAudioMutedState = false, initialVideoMutedState = false; 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"; 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 * show guidance overlay for users on how to give access to camera and/or
* microphone, * microphone,
* @param {string} roomName * @param {string} roomName
* @param {boolean} startScreenSharing - if <tt>true</tt> should start with
* screensharing instead of camera video.
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>} * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
*/ */
createInitialLocalTracksAndConnect(roomName) { createInitialLocalTracksAndConnect(roomName, startScreenSharing) {
let audioAndVideoError, let audioAndVideoError,
audioOnlyError, audioOnlyError,
screenSharingError,
videoOnlyError; videoOnlyError;
JitsiMeetJS.mediaDevices.addEventListener( JitsiMeetJS.mediaDevices.addEventListener(
@ -513,7 +511,30 @@ export default {
); );
// First try to retrieve both audio and video. // First try to retrieve both audio and video.
let tryCreateLocalTracks = createLocalTracks( let tryCreateLocalTracks;
// 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) {devices: ['audio', 'video']}, true)
.catch(err => { .catch(err => {
// If failed then try to retrieve only audio. // If failed then try to retrieve only audio.
@ -531,13 +552,14 @@ export default {
return []; return [];
}); });
}
return Promise.all([ tryCreateLocalTracks, connect(roomName) ]) return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
.then(([tracks, con]) => { .then(([tracks, con]) => {
APP.store.dispatch( APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(false)); mediaPermissionPromptVisibilityChanged(false));
if (audioAndVideoError) { if (audioAndVideoError || audioOnlyError) {
if (audioOnlyError) { if (audioOnlyError || videoOnlyError) {
// If both requests for 'audio' + 'video' and 'audio' // If both requests for 'audio' + 'video' and 'audio'
// only failed, we assume that there is some problems // only failed, we assume that there is some problems
// with user's microphone and show corresponding dialog. // 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]; return [tracks, con];
}); });
}, },
@ -591,7 +625,8 @@ export default {
).then(() => { ).then(() => {
analytics.init(); analytics.init();
return this.createInitialLocalTracksAndConnect( return this.createInitialLocalTracksAndConnect(
options.roomName); options.roomName,
config.startScreenSharing);
}).then(([tracks, con]) => { }).then(([tracks, con]) => {
tracks.forEach(track => { tracks.forEach(track => {
if((track.isAudioTrack() && initialAudioMutedState) if((track.isAudioTrack() && initialAudioMutedState)
@ -1197,7 +1232,6 @@ export default {
/** /**
* Toggles between screensharing and camera video. * Toggles between screensharing and camera video.
* @param {boolean} [shareScreen]
* @param {Object} [options] - Screen sharing options that will be passed to * @param {Object} [options] - Screen sharing options that will be passed to
* createLocalTracks. * createLocalTracks.
* @param {Array<string>} [options.desktopSharingSources] - Array with the * @param {Array<string>} [options.desktopSharingSources] - Array with the
@ -1221,8 +1255,26 @@ export default {
} }
if (!this._untoggleScreenSharing) { 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 externalInstallation = false;
let DSExternalInstallationInProgress = false;
const didHaveVideo = Boolean(localVideo); const didHaveVideo = Boolean(localVideo);
const wasVideoMuted = this.videoMuted; const wasVideoMuted = this.videoMuted;
@ -1236,32 +1288,44 @@ export default {
}, },
listener: (status, url) => { listener: (status, url) => {
switch(status) { switch(status) {
case "waitingForExtension": case "waitingForExtension": {
DSExternalInstallationInProgress = true; DSExternalInstallationInProgress = true;
externalInstallation = true; externalInstallation = true;
APP.UI.showExtensionExternalInstallationDialog( const listener = () => {
url); // 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; break;
case "extensionFound": }
case "extensionFound": {
if (externalInstallation) //close the dialog if (externalInstallation) //close the dialog
$.prompt.close(); $.prompt.close();
break; break;
default: }
default: {
//Unknown status //Unknown status
} }
} }
} }
}).then(([stream]) => { }
}).then(([desktopStream])=> {
// Stores the "untoggle" handler which remembers whether was // Stores the "untoggle" handler which remembers whether was
// there any video before and whether was it muted. // there any video before and whether was it muted.
this._untoggleScreenSharing this._untoggleScreenSharing
= this._turnScreenSharingOff = this._turnScreenSharingOff
.bind(this, didHaveVideo, wasVideoMuted); .bind(this, didHaveVideo, wasVideoMuted);
DSExternalInstallationInProgress = false; desktopStream.on(
// close external installation dialog on success.
if(externalInstallation)
$.prompt.close();
stream.on(
TrackEvents.LOCAL_TRACK_STOPPED, TrackEvents.LOCAL_TRACK_STOPPED,
() => { () => {
// If the stream was stopped during screen sharing // If the stream was stopped during screen sharing
@ -1272,22 +1336,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); return this.useVideoStream(stream);
}).then(() => { }).then(() => {
this.videoSwitchInProgress = false; this.videoSwitchInProgress = false;
JitsiMeetJS.analytics.sendEvent( JitsiMeetJS.analytics.sendEvent('conference.sharingDesktop.start');
'conference.sharingDesktop.start');
logger.log('sharing local desktop'); logger.log('sharing local desktop');
}).catch(err => { }).catch(error => {
// close external installation dialog to show the error.
if(externalInstallation)
$.prompt.close();
this.videoSwitchInProgress = false; 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 // Pawel: With this call I'm trying to preserve the original
// behaviour although it is not clear why would we "untoggle" // behaviour although it is not clear why would we "untoggle"
// on failure. I suppose it was to restore video in case there // on failure. I suppose it was to restore video in case there
@ -1300,13 +1390,49 @@ export default {
this._untoggleScreenSharing(); 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( APP.UI.showExtensionRequiredDialog(
config.desktopSharingFirefoxExtensionURL config.desktopSharingFirefoxExtensionURL
); );
return Promise.reject(err);
return;
} }
// Handling: // Handling:
@ -1317,7 +1443,7 @@ export default {
let dialogTxt; let dialogTxt;
let dialogTitleKey; let dialogTitleKey;
if (err.name === TrackErrors.PERMISSION_DENIED) { if (error.name === TrackErrors.PERMISSION_DENIED) {
dialogTxt = APP.translation.generateTranslationHTML( dialogTxt = APP.translation.generateTranslationHTML(
"dialog.screenSharingPermissionDeniedError"); "dialog.screenSharingPermissionDeniedError");
dialogTitleKey = "dialog.error"; dialogTitleKey = "dialog.error";
@ -1327,12 +1453,7 @@ export default {
dialogTitleKey = "dialog.permissionDenied"; dialogTitleKey = "dialog.permissionDenied";
} }
APP.UI.messageHandler.openDialog( APP.UI.messageHandler.openDialog(dialogTitleKey, dialogTxt, false);
dialogTitleKey, dialogTxt, false);
});
} else {
return this._untoggleScreenSharing();
}
}, },
/** /**
* Setup interaction between conference and UI. * Setup interaction between conference and UI.
@ -1619,17 +1740,6 @@ export default {
APP.UI.updateDTMFSupport(isDTMFSupported); 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.AUDIO_MUTED, muteLocalAudio);
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => { APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
if (this.isAudioOnly() && !muted) { if (this.isAudioOnly() && !muted) {

View File

@ -69,6 +69,7 @@ var config = { // eslint-disable-line no-unused-vars
// page redirection when call is hangup // page redirection when call is hangup
disableSimulcast: false, disableSimulcast: false,
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room. // 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 // startAudioMuted: 10, // every participant after the Nth will start audio muted
// startVideoMuted: 10, // every participant after the Nth will start video muted // startVideoMuted: 10, // every participant after the Nth will start video muted
// defaultLanguage: "en", // defaultLanguage: "en",

View File

@ -332,6 +332,8 @@
"goToStore": "Go to the webstore", "goToStore": "Go to the webstore",
"externalInstallationTitle": "Extension required", "externalInstallationTitle": "Extension required",
"externalInstallationMsg": "You need to install our desktop sharing extension.", "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?", "muteParticipantTitle": "Mute this participant?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute", "muteParticipantButton": "Mute",

View File

@ -1158,15 +1158,34 @@ UI.showExtensionRequiredDialog = function (url) {
* @param url {string} the url of the extension. * @param url {string} the url of the extension.
*/ */
UI.showExtensionExternalInstallationDialog = function (url) { UI.showExtensionExternalInstallationDialog = function (url) {
let openedWindow = null;
let submitFunction = function(e,v){ let submitFunction = function(e,v){
if (v) { if (v) {
e.preventDefault(); 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); eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
}
}; };
messageHandler.openTwoButtonDialog({ 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. * Shows dialog with combined information about camera and microphone errors.

View File

@ -97,12 +97,6 @@ export default {
// changed. // changed.
RESOLUTION_CHANGED: "UI.resolution_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 * Notifies that the button "Cancel" is pressed on the dialog for
* external extension installation. * external extension installation.