fix: Add GUM timeout & improve device permissions

This commit is contained in:
Hristo Terezov 2021-02-01 18:20:39 -06:00
parent 7dc45c28a2
commit a6c6cd6c56
15 changed files with 175 additions and 220 deletions

View File

@ -504,6 +504,8 @@ export default {
let tryCreateLocalTracks; let tryCreateLocalTracks;
const timeout = browser.isElectron() ? 15000 : 60000;
// FIXME is there any simpler way to rewrite this spaghetti below ? // FIXME is there any simpler way to rewrite this spaghetti below ?
if (options.startScreenSharing) { if (options.startScreenSharing) {
tryCreateLocalTracks = this._createDesktopTrack() tryCreateLocalTracks = this._createDesktopTrack()
@ -512,7 +514,10 @@ export default {
return [ desktopStream ]; return [ desktopStream ];
} }
return createLocalTracksF({ devices: [ 'audio' ] }, true) return createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
.then(([ audioStream ]) => .then(([ audioStream ]) =>
[ desktopStream, audioStream ]) [ desktopStream, audioStream ])
.catch(error => { .catch(error => {
@ -526,7 +531,10 @@ export default {
errors.screenSharingError = error; errors.screenSharingError = error;
return requestedAudio return requestedAudio
? createLocalTracksF({ devices: [ 'audio' ] }, true) ? createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
: []; : [];
}) })
.catch(error => { .catch(error => {
@ -538,15 +546,33 @@ export default {
// Resolve with no tracks // Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]); tryCreateLocalTracks = Promise.resolve([]);
} else { } else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true) tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout
}, true)
.catch(err => { .catch(err => {
if (requestedAudio && requestedVideo) { if (requestedAudio && requestedVideo) {
// Try audio only... // Try audio only...
errors.audioAndVideoError = err; errors.audioAndVideoError = err;
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
// In this case we expect that the permission prompt is still visible. There is no point of
// executing GUM with different source. Also at the time of writting the following
// inconsistency have been noticed in some browsers - if the permissions prompt is visible
// and another GUM is executed the prompt does not change its content but if the user
// clicks allow the user action isassociated with the latest GUM call.
errors.audioOnlyError = err;
errors.videoOnlyError = err;
return [];
}
return ( return (
createLocalTracksF({ devices: [ 'audio' ] }, true)); createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true));
} else if (requestedAudio && !requestedVideo) { } else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err; errors.audioOnlyError = err;
@ -567,7 +593,10 @@ export default {
// Try video only... // Try video only...
return requestedVideo return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true) ? createLocalTracksF({
devices: [ 'video' ],
timeout
}, true)
: []; : [];
}) })
.catch(err => { .catch(err => {

View File

@ -180,6 +180,7 @@
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.", "cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
"cameraNotSendingDataTitle": "Unable to access camera", "cameraNotSendingDataTitle": "Unable to access 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.", "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.",
"cameraTimeoutError": "Could not start video source. Timeout occured!",
"cameraUnknownError": "Cannot use camera for an unknown reason.", "cameraUnknownError": "Cannot use camera for an unknown reason.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.", "cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
"Cancel": "Cancel", "Cancel": "Cancel",
@ -233,6 +234,7 @@
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level", "micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
"micNotSendingDataTitle": "Your mic is muted by your system settings", "micNotSendingDataTitle": "Your mic is muted by your system settings",
"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.", "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.",
"micTimeoutError": "Could not start audio source. Timeout occured!",
"micUnknownError": "Cannot use microphone for an unknown reason.", "micUnknownError": "Cannot use microphone for an unknown reason.",
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.", "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?", "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
@ -810,7 +812,7 @@
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.", "androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.", "chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions.", "edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions.",
"electronGrantPermissions": "Please grant permissions to use your camera and microphone", "electronGrantPermissions": "Trying to access your camera and microphone",
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.", "firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.", "iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone", "nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",

View File

@ -84,3 +84,13 @@ export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';
* } * }
*/ */
export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE'; export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE';
/**
* The type of Redux action which signals that the device permissions have changed.
*
* {
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
* permissions: Object
* }
*/
export const DEVICE_PERMISSIONS_CHANGED = 'DEVICE_PERMISSIONS_CHANGED';

View File

@ -7,6 +7,7 @@ import {
import { import {
ADD_PENDING_DEVICE_REQUEST, ADD_PENDING_DEVICE_REQUEST,
CHECK_AND_NOTIFY_FOR_NEW_DEVICE, CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
DEVICE_PERMISSIONS_CHANGED,
NOTIFY_CAMERA_ERROR, NOTIFY_CAMERA_ERROR,
NOTIFY_MIC_ERROR, NOTIFY_MIC_ERROR,
REMOVE_PENDING_DEVICE_REQUESTS, REMOVE_PENDING_DEVICE_REQUESTS,
@ -320,3 +321,19 @@ export function checkAndNotifyForNewDevice(newDevices, oldDevices) {
oldDevices oldDevices
}; };
} }
/**
* Signals that the device permissions have changed.
*
* @param {Object} permissions - Object with the permissions.
* @returns {{
* type: DEVICE_PERMISSIONS_CHANGED,
* permissions: Object
* }}
*/
export function devicePermissionsChanged(permissions) {
return {
type: DEVICE_PERMISSIONS_CHANGED,
permissions
};
}

View File

@ -5,7 +5,8 @@ import { processExternalDeviceRequest } from '../../device-selection';
import { showNotification, showWarningNotification } from '../../notifications'; import { showNotification, showWarningNotification } from '../../notifications';
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions'; import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions'; import { isPrejoinPageVisible } from '../../prejoin/functions';
import { JitsiTrackErrors } from '../lib-jitsi-meet'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { updateSettings } from '../settings'; import { updateSettings } from '../settings';
@ -18,6 +19,7 @@ import {
UPDATE_DEVICE_LIST UPDATE_DEVICE_LIST
} from './actionTypes'; } from './actionTypes';
import { import {
devicePermissionsChanged,
removePendingDeviceRequests, removePendingDeviceRequests,
setAudioInputDevice, setAudioInputDevice,
setVideoInputDevice setVideoInputDevice
@ -35,17 +37,25 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError', [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError', [JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError', [JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError' [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
[JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
}, },
camera: { camera: {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError', [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError', [JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError', [JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError', [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError' [JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
[JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
} }
}; };
/**
* A listener for device permissions changed reported from lib-jitsi-meet.
*/
let permissionsListener;
/** /**
* Logs the current device list. * Logs the current device list.
* *
@ -73,6 +83,36 @@ function logDeviceList(deviceList) {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case APP_WILL_MOUNT: {
const _permissionsListener = permissions => {
store.dispatch(devicePermissionsChanged(permissions));
};
const { mediaDevices } = JitsiMeetJS;
permissionsListener = _permissionsListener;
mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
Promise.all([
mediaDevices.isDevicePermissionGranted('audio'),
mediaDevices.isDevicePermissionGranted('video')
])
.then(results => {
_permissionsListener({
audio: results[0],
video: results[1]
});
})
.catch(() => {
// Ignore errors.
});
break;
}
case APP_WILL_UNMOUNT:
if (typeof permissionsListener === 'function') {
JitsiMeetJS.mediaDevices.removeEventListener(
JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
permissionsListener = undefined;
}
break;
case NOTIFY_CAMERA_ERROR: { case NOTIFY_CAMERA_ERROR: {
if (typeof APP !== 'object' || !action.error) { if (typeof APP !== 'object' || !action.error) {
break; break;

View File

@ -2,6 +2,7 @@ import { ReducerRegistry } from '../redux';
import { import {
ADD_PENDING_DEVICE_REQUEST, ADD_PENDING_DEVICE_REQUEST,
DEVICE_PERMISSIONS_CHANGED,
REMOVE_PENDING_DEVICE_REQUESTS, REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE, SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE,
@ -16,7 +17,11 @@ const DEFAULT_STATE = {
audioOutput: [], audioOutput: [],
videoInput: [] videoInput: []
}, },
pendingRequests: [] pendingRequests: [],
permissions: {
audio: false,
video: false
}
}; };
/** /**
@ -68,6 +73,12 @@ ReducerRegistry.register(
return state; return state;
} }
case DEVICE_PERMISSIONS_CHANGED: {
return {
...state,
permissions: action.permissions
};
}
default: default:
return state; return state;
} }

View File

@ -13,9 +13,10 @@ const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
* @param {string} type - The media type of track being created. Expected values * @param {string} type - The media type of track being created. Expected values
* are "video" or "audio". * are "video" or "audio".
* @param {string} deviceId - The id of the target media source. * @param {string} deviceId - The id of the target media source.
* @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
* @returns {Promise<JitsiLocalTrack>} * @returns {Promise<JitsiLocalTrack>}
*/ */
export function createLocalTrack(type: string, deviceId: string) { export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
return ( return (
JitsiMeetJS.createLocalTracks({ JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId, cameraDeviceId: deviceId,
@ -24,7 +25,8 @@ export function createLocalTrack(type: string, deviceId: string) {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
firefox_fake_device: firefox_fake_device:
window.config && window.config.firefox_fake_device, window.config && window.config.firefox_fake_device,
micDeviceId: deviceId micDeviceId: deviceId,
timeout
}) })
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack)); .then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
} }

View File

@ -62,6 +62,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
* and/or 'video'. * and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or * @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings. * {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet * @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a * should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event. * corresponding event.
@ -71,6 +72,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
*/ */
export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) { export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) {
let { cameraDeviceId, micDeviceId } = options; let { cameraDeviceId, micDeviceId } = options;
const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options;
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the // TODO The app's settings should go in the redux store and then the
@ -105,16 +107,16 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
cameraDeviceId, cameraDeviceId,
constraints, constraints,
desktopSharingFrameRate, desktopSharingFrameRate,
desktopSharingSourceDevice: desktopSharingSourceDevice,
options.desktopSharingSourceDevice, desktopSharingSources,
desktopSharingSources: options.desktopSharingSources,
// Copy array to avoid mutations inside library. // Copy array to avoid mutations inside library.
devices: options.devices.slice(0), devices: options.devices.slice(0),
effects, effects,
firefox_fake_device, // eslint-disable-line camelcase firefox_fake_device, // eslint-disable-line camelcase
micDeviceId, micDeviceId,
resolution resolution,
timeout
}, },
firePermissionPromptIsShownEvent) firePermissionPromptIsShownEvent)
.catch(err => { .catch(err => {

View File

@ -6,7 +6,6 @@ import AbstractDialogTab, {
type Props as AbstractDialogTabProps type Props as AbstractDialogTabProps
} from '../../base/dialog/components/web/AbstractDialogTab'; } from '../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../base/i18n/functions'; import { translate } from '../../base/i18n/functions';
import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions'; import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
import logger from '../logger'; import logger from '../logger';
@ -41,6 +40,16 @@ export type Props = {
*/ */
disableDeviceChange: boolean, disableDeviceChange: boolean,
/**
* Whether or not the audio permission was granted.
*/
hasAudioPermission: boolean,
/**
* Whether or not the audio permission was granted.
*/
hasVideoPermission: boolean,
/** /**
* If true, the audio meter will not display. Necessary for browsers or * If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a * configurations that do not support local stats to prevent a
@ -87,16 +96,6 @@ export type Props = {
*/ */
type State = { type State = {
/**
* Whether or not the audio permission was granted.
*/
hasAudioPermission: boolean,
/**
* Whether or not the audio permission was granted.
*/
hasVideoPermission: boolean,
/** /**
* The JitsiTrack to use for previewing audio input. * The JitsiTrack to use for previewing audio input.
*/ */
@ -141,8 +140,6 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
super(props); super(props);
this.state = { this.state = {
hasAudioPermission: false,
hasVideoPermission: false,
previewAudioTrack: null, previewAudioTrack: null,
previewVideoTrack: null, previewVideoTrack: null,
previewVideoTrackError: null previewVideoTrackError: null
@ -170,27 +167,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* video input previews. * video input previews.
* *
* @param {Object} prevProps - Previous props this component received. * @param {Object} prevProps - Previous props this component received.
* @param {Object} prevState - Previous state this component had.
* @returns {void} * @returns {void}
*/ */
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps) {
const { previewAudioTrack, previewVideoTrack } = prevState;
if ((!previewAudioTrack && this.state.previewAudioTrack)
|| (!previewVideoTrack && this.state.previewVideoTrack)) {
Promise.all([
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio'),
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video')
]).then(r => {
const [ hasAudioPermission, hasVideoPermission ] = r;
this.setState({
hasAudioPermission,
hasVideoPermission
});
});
}
if (prevProps.selectedAudioInputId if (prevProps.selectedAudioInputId
!== this.props.selectedAudioInputId) { !== this.props.selectedAudioInputId) {
this._createAudioInputTrack(this.props.selectedAudioInputId); this._createAudioInputTrack(this.props.selectedAudioInputId);
@ -258,7 +237,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
*/ */
_createAudioInputTrack(deviceId) { _createAudioInputTrack(deviceId) {
return this._disposeAudioInputPreview() return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId)) .then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => { .then(jitsiLocalTrack => {
if (this._unMounted) { if (this._unMounted) {
jitsiLocalTrack.dispose(); jitsiLocalTrack.dispose();
@ -286,7 +265,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
*/ */
_createVideoInputTrack(deviceId) { _createVideoInputTrack(deviceId) {
return this._disposeVideoInputPreview() return this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId)) .then(() => createLocalTrack('video', deviceId, 5000))
.then(jitsiLocalTrack => { .then(jitsiLocalTrack => {
if (!jitsiLocalTrack) { if (!jitsiLocalTrack) {
return Promise.reject(); return Promise.reject();
@ -360,8 +339,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {Array<ReactElement>} DeviceSelector instances. * @returns {Array<ReactElement>} DeviceSelector instances.
*/ */
_renderSelectors() { _renderSelectors() {
const { availableDevices } = this.props; const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
const { hasAudioPermission, hasVideoPermission } = this.state;
const configurations = [ const configurations = [
{ {

View File

@ -32,6 +32,7 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
const state = toState(stateful); const state = toState(stateful);
const settings = state['features/base/settings']; const settings = state['features/base/settings'];
const { conference } = state['features/base/conference']; const { conference } = state['features/base/conference'];
const { permissions } = state['features/base/devices'];
let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported(); let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
let selectedAudioInputId = settings.micDeviceId; let selectedAudioInputId = settings.micDeviceId;
let selectedAudioOutputId = getAudioOutputDeviceId(); let selectedAudioOutputId = getAudioOutputDeviceId();
@ -55,6 +56,8 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
disableAudioInputChange, disableAudioInputChange,
disableDeviceChange: disableDeviceChange:
!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(), !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
hasAudioPermission: permissions.audio,
hasVideoPermission: permissions.video,
hideAudioInputPreview: hideAudioInputPreview:
!JitsiMeetJS.isCollectingLocalStats(), !JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices hideAudioOutputSelect: !JitsiMeetJS.mediaDevices

View File

@ -182,9 +182,7 @@ class AudioSettingsContent extends Component<Props, State> {
this._disposeTracks(this.state.audioTracks); this._disposeTracks(this.state.audioTracks);
const audioTracks = await createLocalAudioTracks( const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000);
this.props.microphoneDevices
);
if (this._componentWasUnmounted) { if (this._componentWasUnmounted) {
this._disposeTracks(audioTracks); this._disposeTracks(audioTracks);

View File

@ -85,9 +85,7 @@ class VideoSettingsContent extends Component<Props, State> {
async _setTracks() { async _setTracks() {
this._disposeTracks(this.state.trackData); this._disposeTracks(this.state.trackData);
const trackData = await createLocalVideoTracks( const trackData = await createLocalVideoTracks(this.props.videoDeviceIds, 5000);
this.props.videoDeviceIds,
);
// In case the component gets unmounted before the tracks are created // In case the component gets unmounted before the tracks are created
// avoid a leak by not setting the state // avoid a leak by not setting the state

View File

@ -156,11 +156,12 @@ export function getProfileTabProps(stateful: Object | Function) {
* all the video jitsiTracks and appropriate errors for the given device ids. * all the video jitsiTracks and appropriate errors for the given device ids.
* *
* @param {string[]} ids - The list of the camera ids for wich to create tracks. * @param {string[]} ids - The list of the camera ids for wich to create tracks.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
* *
* @returns {Promise<Object[]>} * @returns {Promise<Object[]>}
*/ */
export function createLocalVideoTracks(ids: string[]) { export function createLocalVideoTracks(ids: string[], timeout: ?number) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId) return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
.then(jitsiTrack => { .then(jitsiTrack => {
return { return {
jitsiTrack, jitsiTrack,
@ -182,6 +183,7 @@ export function createLocalVideoTracks(ids: string[]) {
* the audio track and the corresponding audio device information. * the audio track and the corresponding audio device information.
* *
* @param {Object[]} devices - A list of microphone devices. * @param {Object[]} devices - A list of microphone devices.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
* @returns {Promise<{ * @returns {Promise<{
* deviceId: string, * deviceId: string,
* hasError: boolean, * hasError: boolean,
@ -189,14 +191,14 @@ export function createLocalVideoTracks(ids: string[]) {
* label: string * label: string
* }[]>} * }[]>}
*/ */
export function createLocalAudioTracks(devices: Object[]) { export function createLocalAudioTracks(devices: Object[], timeout: ?number) {
return Promise.all( return Promise.all(
devices.map(async ({ deviceId, label }) => { devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null; let jitsiTrack = null;
let hasError = false; let hasError = false;
try { try {
jitsiTrack = await createLocalTrack('audio', deviceId); jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
} catch (err) { } catch (err) {
hasError = true; hasError = true;
} }

View File

@ -7,24 +7,22 @@ import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components'; import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import { getMediaPermissionPromptVisibility } from '../../../overlay';
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings'; import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
import { isAudioSettingsButtonDisabled } from '../../functions'; import { isAudioSettingsButtonDisabled } from '../../functions';
import AudioMuteButton from '../AudioMuteButton'; import AudioMuteButton from '../AudioMuteButton';
type Props = { type Props = {
/**
* Indicates whether audio permissions have been granted or denied.
*/
hasPermissions: boolean,
/** /**
* Click handler for the small icon. Opens audio options. * Click handler for the small icon. Opens audio options.
*/ */
onAudioOptionsClick: Function, onAudioOptionsClick: Function,
/**
* Whether the permission prompt is visible or not.
* Useful for enabling the button on permission grant.
*/
permissionPromptVisibility: boolean,
/** /**
* If the button should be disabled. * If the button should be disabled.
*/ */
@ -34,83 +32,15 @@ type Props = {
* Flag controlling the visibility of the button. * Flag controlling the visibility of the button.
* AudioSettings popup is disabled on mobile browsers. * AudioSettings popup is disabled on mobile browsers.
*/ */
visible: boolean, visible: boolean
}; };
type State = {
/**
* If there are permissions for audio devices.
*/
hasPermissions: boolean,
}
/** /**
* Button used for audio & audio settings. * Button used for audio & audio settings.
* *
* @returns {ReactElement} * @returns {ReactElement}
*/ */
class AudioSettingsButton extends Component<Props, State> { class AudioSettingsButton extends Component<Props> {
_isMounted: boolean;
/**
* Initializes a new {@code AudioSettingsButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this._isMounted = true;
this.state = {
hasPermissions: false
};
}
/**
* Updates device permissions.
*
* @returns {Promise<void>}
*/
async _updatePermissions() {
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
'audio',
);
this._isMounted && this.setState({
hasPermissions
});
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updatePermissions();
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
this._updatePermissions();
}
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._isMounted = false;
}
/** /**
* Implements React's {@link Component#render}. * Implements React's {@link Component#render}.
@ -118,8 +48,8 @@ class AudioSettingsButton extends Component<Props, State> {
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
const { isDisabled, onAudioOptionsClick, visible } = this.props; const { hasPermissions, isDisabled, onAudioOptionsClick, visible } = this.props;
const settingsDisabled = !this.state.hasPermissions const settingsDisabled = !hasPermissions
|| isDisabled || isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported(); || !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
@ -143,9 +73,11 @@ class AudioSettingsButton extends Component<Props, State> {
* @returns {Object} * @returns {Object}
*/ */
function mapStateToProps(state) { function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
return { return {
hasPermissions: permissions.audio,
isDisabled: isAudioSettingsButtonDisabled(state), isDisabled: isAudioSettingsButtonDisabled(state),
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
visible: !isMobileBrowser() visible: !isMobileBrowser()
}; };
} }

View File

@ -4,11 +4,9 @@ import React, { Component } from 'react';
import { isMobileBrowser } from '../../../base/environment/utils'; import { isMobileBrowser } from '../../../base/environment/utils';
import { IconArrowDown } from '../../../base/icons'; import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components'; import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import { getLocalJitsiVideoTrack } from '../../../base/tracks'; import { getLocalJitsiVideoTrack } from '../../../base/tracks';
import { getMediaPermissionPromptVisibility } from '../../../overlay';
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings'; import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
import { isVideoSettingsButtonDisabled } from '../../functions'; import { isVideoSettingsButtonDisabled } from '../../functions';
import VideoMuteButton from '../VideoMuteButton'; import VideoMuteButton from '../VideoMuteButton';
@ -21,10 +19,9 @@ type Props = {
onVideoOptionsClick: Function, onVideoOptionsClick: Function,
/** /**
* Whether the permission prompt is visible or not. * Indicates whether video permissions have been granted or denied.
* Useful for enabling the button on initial permission grant.
*/ */
permissionPromptVisibility: boolean, hasPermissions: boolean,
/** /**
* Whether there is a video track or not. * Whether there is a video track or not.
@ -42,15 +39,7 @@ type Props = {
* as mobile devices do not support capture of more than one * as mobile devices do not support capture of more than one
* camera at a time. * camera at a time.
*/ */
visible: boolean, visible: boolean
};
type State = {
/**
* Whether the app has video permissions or not.
*/
hasPermissions: boolean,
}; };
/** /**
@ -58,23 +47,7 @@ type State = {
* *
* @returns {ReactElement} * @returns {ReactElement}
*/ */
class VideoSettingsButton extends Component<Props, State> { class VideoSettingsButton extends Component<Props> {
_isMounted: boolean;
/**
* Initializes a new {@code VideoSettingsButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this._isMounted = true;
this.state = {
hasPermissions: false
};
}
/** /**
* Returns true if the settings icon is disabled. * Returns true if the settings icon is disabled.
@ -82,53 +55,9 @@ class VideoSettingsButton extends Component<Props, State> {
* @returns {boolean} * @returns {boolean}
*/ */
_isIconDisabled() { _isIconDisabled() {
const { hasVideoTrack, isDisabled } = this.props; const { hasPermissions, hasVideoTrack, isDisabled } = this.props;
return (!this.state.hasPermissions || isDisabled) && !hasVideoTrack; return (!hasPermissions || isDisabled) && !hasVideoTrack;
}
/**
* Updates device permissions.
*
* @returns {Promise<void>}
*/
async _updatePermissions() {
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
'video',
);
this._isMounted && this.setState({
hasPermissions
});
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updatePermissions();
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
this._updatePermissions();
}
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._isMounted = false;
} }
/** /**
@ -159,10 +88,12 @@ class VideoSettingsButton extends Component<Props, State> {
* @returns {Object} * @returns {Object}
*/ */
function mapStateToProps(state) { function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
return { return {
hasPermissions: permissions.video,
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)), hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state), isDisabled: isVideoSettingsButtonDisabled(state),
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
visible: !isMobileBrowser() visible: !isMobileBrowser()
}; };
} }