Merge pull request #701 from tsareg/gum_permission_dialog_guidance

Show overlay with guidance for gUM permission prompts
This commit is contained in:
yanas 2016-06-24 11:17:38 -05:00 committed by GitHub
commit 1d393f5786
6 changed files with 243 additions and 105 deletions

View File

@ -56,6 +56,58 @@ 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;
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMeetJS.events.mediaDevices.PERMISSION_PROMPT_IS_SHOWN,
browser => APP.UI.showUserMediaPermissionsGuidanceOverlay(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 => {
// If audio failed too then just return empty array for tracks.
audioOnlyError = err;
return [];
});
return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
.then(([tracks, con]) => {
APP.UI.hideUserMediaPermissionsGuidanceOverlay();
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, null);
} 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
@ -182,29 +234,39 @@ function hangup (requestFeedback = false) {
/**
* Create local tracks of specified types.
* @param {string[]} devices - required track types ('audio', 'video' etc.)
* @param {string|null} [cameraDeviceId] - camera device id, if undefined - one
* from settings will be used
* @param {string|null} [micDeviceId] - microphone device id, if undefined - one
* from settings will be used
* @param {Object} options
* @param {string[]} options.devices - required track types
* ('audio', 'video' etc.)
* @param {string|null} (options.cameraDeviceId) - camera device id, if
* undefined - one from settings will be used
* @param {string|null} (options.micDeviceId) - microphone device id, if
* undefined - one from settings will be used
* @param {boolean} (checkForPermissionPrompt) - if lib-jitsi-meet should check
* for gUM permission prompt
* @returns {Promise<JitsiLocalTrack[]>}
*/
function createLocalTracks (devices, cameraDeviceId, micDeviceId) {
return JitsiMeetJS.createLocalTracks({
function createLocalTracks (options, checkForPermissionPrompt) {
options || (options = {});
return JitsiMeetJS
.createLocalTracks({
// copy array to avoid mutations inside library
devices: devices.slice(0),
devices: options.devices.slice(0),
resolution: config.resolution,
cameraDeviceId: typeof cameraDeviceId === 'undefined'
|| cameraDeviceId === null
cameraDeviceId: typeof options.cameraDeviceId === 'undefined' ||
options.cameraDeviceId === null
? APP.settings.getCameraDeviceId()
: cameraDeviceId,
micDeviceId: typeof micDeviceId === 'undefined' || micDeviceId === null
: options.cameraDeviceId,
micDeviceId: typeof options.micDeviceId === 'undefined' ||
options.micDeviceId === null
? APP.settings.getMicDeviceId()
: micDeviceId,
: options.micDeviceId,
// adds any ff fake device settings if any
firefox_fake_device: config.firefox_fake_device
}).catch(function (err) {
console.error('failed to create local tracks', ...devices, err);
}, checkForPermissionPrompt)
.catch(function (err) {
console.error(
'failed to create local tracks', options.devices, err);
return Promise.reject(err);
});
}
@ -406,7 +468,6 @@ export default {
* @returns {Promise}
*/
init(options) {
let self = this;
this.roomName = options.roomName;
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE);
@ -432,39 +493,9 @@ export default {
};
}
let audioAndVideoError, audioOnlyError;
return JitsiMeetJS.init(config).then(() => {
return Promise.all([
// try to retrieve audio and video
createLocalTracks(['audio', 'video'])
// if failed then try to retrieve only audio
.catch(err => {
audioAndVideoError = err;
return createLocalTracks(['audio']);
})
// if audio also failed then just return empty array
.catch(err => {
audioOnlyError = err;
return [];
}),
connect(options.roomName)
]);
}).then(([tracks, con]) => {
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, null);
} 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 JitsiMeetJS.init(config)
.then(() => createInitialLocalTracksAndConnect(options.roomName))
.then(([tracks, con]) => {
console.log('initialized with %s local tracks', tracks.length);
APP.connection = connection = con;
this._createRoom(tracks);
@ -486,8 +517,8 @@ export default {
if (config.iAmRecorder)
this.recorder = new Recorder();
// XXX The API will take care of disconnecting from the XMPP server
// (and, thus, leaving the room) on unload.
// XXX The API will take care of disconnecting from the XMPP
// server (and, thus, leaving the room) on unload.
return new Promise((resolve, reject) => {
(new ConferenceConnector(resolve, reject)).connect();
});
@ -834,7 +865,7 @@ export default {
this.videoSwitchInProgress = true;
if (shareScreen) {
createLocalTracks(['desktop']).then(([stream]) => {
createLocalTracks({ devices: ['desktop'] }).then(([stream]) => {
stream.on(
TrackEvents.LOCAL_TRACK_STOPPED,
() => {
@ -891,7 +922,7 @@ export default {
APP.UI.messageHandler.openDialog(dialogTitle, dialogTxt, false);
});
} else {
createLocalTracks(['video']).then(
createLocalTracks({ devices: ['video'] }).then(
([stream]) => this.useVideoStream(stream)
).then(() => {
this.videoSwitchInProgress = false;
@ -1247,7 +1278,11 @@ export default {
APP.UI.addListener(
UIEvents.VIDEO_DEVICE_CHANGED,
(cameraDeviceId) => {
createLocalTracks(['video'], cameraDeviceId, null)
createLocalTracks({
devices: ['video'],
cameraDeviceId: cameraDeviceId,
micDeviceId: null
})
.then(([stream]) => {
this.useVideoStream(stream);
console.log('switched local video device');
@ -1263,7 +1298,11 @@ export default {
APP.UI.addListener(
UIEvents.AUDIO_DEVICE_CHANGED,
(micDeviceId) => {
createLocalTracks(['audio'], null, micDeviceId)
createLocalTracks({
devices: ['audio'],
cameraDeviceId: null,
micDeviceId: micDeviceId
})
.then(([stream]) => {
this.useAudioStream(stream);
console.log('switched local audio device');

View File

@ -11,6 +11,10 @@
display: block;
}
.overlay_transparent {
background: none;
}
.overlay_container {
width: 100%;
height: 100%;
@ -49,3 +53,14 @@
margin-top: 20px;
float: left;
}
.overlay_text_small {
font-size: 18px;
}
.overlay_icon {
position: relative;
z-index: 1013;
float: none;
font-size: 100px;
}

View File

@ -12,6 +12,16 @@
"defaultNickname": "ex. Jane Pink",
"defaultLink": "e.g. __url__",
"calling": "Calling __name__ ...",
"userMedia": {
"react-nativeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>Allow</i> button",
"chromeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>Allow</i> button",
"androidGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>Allow</i> button",
"firefoxGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>Share Selected Device</i> button",
"operaGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>Allow</i> button",
"iexplorerGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>OK</i> button",
"safariGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <i>OK</i> button",
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone"
},
"keyboardShortcuts": {
"keyboardShortcuts": "Keyboard shortcuts:",
"raiseHand": "Raise your hand.",
@ -247,11 +257,11 @@
"cameraErrorPresent": "There was an error connecting to your camera.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
"cameraUnknownError": "Cannot use camera for a unknown reason.",
"cameraPermissionDeniedError": "You have not granted permission to use your camera.",
"cameraPermissionDeniedError": "You have not granted permission to use your camera. You can still join the conference but others won't see you. Use the camera button in the address bar to fix this.",
"cameraNotFoundError": "Requested camera was not found.",
"cameraConstraintFailedError": "Yor camera does not satisfy some of required constraints.",
"micUnknownError": "Cannot use microphone for a unknown reason.",
"micPermissionDeniedError": "You have not granted permission to use your microphone.",
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micNotFoundError": "Requested microphone was not found.",
"micConstraintFailedError": "Yor microphone does not satisfy some of required constraints."
},

View File

@ -15,6 +15,7 @@ import CQEvents from '../../service/connectionquality/CQEvents';
import EtherpadManager from './etherpad/Etherpad';
import SharedVideoManager from './shared_video/SharedVideo';
import Recording from "./recording/Recording";
import GumPermissionsOverlay from './gum_overlay/UserMediaPermissionsGuidanceOverlay';
import VideoLayout from "./videolayout/VideoLayout";
import FilmStrip from "./videolayout/FilmStrip";
@ -1415,6 +1416,22 @@ UI.hideRingOverLay = function () {
FilmStrip.toggleFilmStrip(true);
};
/**
* Shows browser-specific overlay with guidance how to proceed with gUM prompt.
* @param {string} browser - name of browser for which to show the guidance
* overlay.
*/
UI.showUserMediaPermissionsGuidanceOverlay = function (browser) {
GumPermissionsOverlay.show(browser);
};
/**
* Hides browser-specific overlay with guidance how to proceed with gUM prompt.
*/
UI.hideUserMediaPermissionsGuidanceOverlay = function () {
GumPermissionsOverlay.hide();
};
/**
* Shows or hides the keyboard shortcuts panel, depending on the current state.'
*/

View File

@ -0,0 +1,46 @@
/* global $, APP, JitsiMeetJS */
let $overlay;
/**
* Internal function that constructs overlay with guidance how to proceed with
* gUM prompt.
* @param {string} browser - name of browser for which to construct the
* guidance overlay.
*/
function buildOverlayHtml(browser) {
$overlay = $(`
<div class='overlay_container'>
<div class='overlay overlay_transparent' />
<div class='overlay_content'>
<span class="overlay_icon icon-microphone"></span>
<span class="overlay_icon icon-camera"></span>
<span data-i18n='[html]userMedia.${browser}GrantPermissions'
class='overlay_text overlay_text_small'></span>
</div>
</div>`);
APP.translation.translateElement($overlay);
}
export default {
/**
* Shows browser-specific overlay with guidance how to proceed with
* gUM prompt.
* @param {string} browser - name of browser for which to show the
* guidance overlay.
*/
show(browser) {
!$overlay && buildOverlayHtml(browser);
!$overlay.parents('body').length && $overlay.appendTo('body');
},
/**
* Hides browser-specific overlay with guidance how to proceed with
* gUM prompt.
*/
hide() {
$overlay && $overlay.detach();
}
};

View File

@ -191,8 +191,11 @@ export default {
if (audioRequested && videoRequested) {
// First we try to create both audio and video tracks together.
return createLocalTracks(
['audio', 'video'], cameraDeviceId, micDeviceId)
return createLocalTracks({
devices: ['audio', 'video'],
cameraDeviceId: cameraDeviceId,
micDeviceId: micDeviceId
})
// If we fail to do this, try to create them separately.
.catch(() => Promise.all([
createAudioTrack(false).then(([stream]) => stream),
@ -215,7 +218,11 @@ export default {
}
function createAudioTrack(showError) {
return createLocalTracks(['audio'], null, micDeviceId)
return createLocalTracks({
devices: ['audio'],
cameraDeviceId: null,
micDeviceId: micDeviceId
})
.catch(err => {
audioTrackError = err;
showError && APP.UI.showDeviceErrorDialog(err, null);
@ -224,7 +231,11 @@ export default {
}
function createVideoTrack(showError) {
return createLocalTracks(['video'], cameraDeviceId, null)
return createLocalTracks({
devices: ['video'],
cameraDeviceId: cameraDeviceId,
micDeviceId: null
})
.catch(err => {
videoTrackError = err;
showError && APP.UI.showDeviceErrorDialog(null, err);