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;
const timeout = browser.isElectron() ? 15000 : 60000;
// FIXME is there any simpler way to rewrite this spaghetti below ?
if (options.startScreenSharing) {
tryCreateLocalTracks = this._createDesktopTrack()
@ -512,7 +514,10 @@ export default {
return [ desktopStream ];
}
return createLocalTracksF({ devices: [ 'audio' ] }, true)
return createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
.then(([ audioStream ]) =>
[ desktopStream, audioStream ])
.catch(error => {
@ -526,7 +531,10 @@ export default {
errors.screenSharingError = error;
return requestedAudio
? createLocalTracksF({ devices: [ 'audio' ] }, true)
? createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
: [];
})
.catch(error => {
@ -538,15 +546,33 @@ export default {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout
}, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
// Try audio only...
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 (
createLocalTracksF({ devices: [ 'audio' ] }, true));
createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@ -567,7 +593,10 @@ export default {
// Try video only...
return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true)
? createLocalTracksF({
devices: [ 'video' ],
timeout
}, true)
: [];
})
.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.",
"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.",
"cameraTimeoutError": "Could not start video source. Timeout occured!",
"cameraUnknownError": "Cannot use camera for an unknown reason.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
"Cancel": "Cancel",
@ -233,6 +234,7 @@
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
"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.",
"micTimeoutError": "Could not start audio source. Timeout occured!",
"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.",
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
@ -810,7 +812,7 @@
"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.",
"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.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
"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';
/**
* 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 {
ADD_PENDING_DEVICE_REQUEST,
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
DEVICE_PERMISSIONS_CHANGED,
NOTIFY_CAMERA_ERROR,
NOTIFY_MIC_ERROR,
REMOVE_PENDING_DEVICE_REQUESTS,
@ -320,3 +321,19 @@ export function checkAndNotifyForNewDevice(newDevices, 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 { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
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 { updateSettings } from '../settings';
@ -18,6 +19,7 @@ import {
UPDATE_DEVICE_LIST
} from './actionTypes';
import {
devicePermissionsChanged,
removePendingDeviceRequests,
setAudioInputDevice,
setVideoInputDevice
@ -35,17 +37,25 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError'
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
[JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
},
camera: {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
[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.
*
@ -73,6 +83,36 @@ function logDeviceList(deviceList) {
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
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: {
if (typeof APP !== 'object' || !action.error) {
break;

View File

@ -2,6 +2,7 @@ import { ReducerRegistry } from '../redux';
import {
ADD_PENDING_DEVICE_REQUEST,
DEVICE_PERMISSIONS_CHANGED,
REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
@ -16,7 +17,11 @@ const DEFAULT_STATE = {
audioOutput: [],
videoInput: []
},
pendingRequests: []
pendingRequests: [],
permissions: {
audio: false,
video: false
}
};
/**
@ -68,6 +73,12 @@ ReducerRegistry.register(
return state;
}
case DEVICE_PERMISSIONS_CHANGED: {
return {
...state,
permissions: action.permissions
};
}
default:
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
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
* @returns {Promise<JitsiLocalTrack>}
*/
export function createLocalTrack(type: string, deviceId: string) {
export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
return (
JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId,
@ -24,7 +25,8 @@ export function createLocalTrack(type: string, deviceId: string) {
// eslint-disable-next-line camelcase
firefox_fake_device:
window.config && window.config.firefox_fake_device,
micDeviceId: deviceId
micDeviceId: deviceId,
timeout
})
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -85,9 +85,7 @@ class VideoSettingsContent extends Component<Props, State> {
async _setTracks() {
this._disposeTracks(this.state.trackData);
const trackData = await createLocalVideoTracks(
this.props.videoDeviceIds,
);
const trackData = await createLocalVideoTracks(this.props.videoDeviceIds, 5000);
// In case the component gets unmounted before the tracks are created
// 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.
*
* @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[]>}
*/
export function createLocalVideoTracks(ids: string[]) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId)
export function createLocalVideoTracks(ids: string[], timeout: ?number) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
.then(jitsiTrack => {
return {
jitsiTrack,
@ -182,6 +183,7 @@ export function createLocalVideoTracks(ids: string[]) {
* the audio track and the corresponding audio device information.
*
* @param {Object[]} devices - A list of microphone devices.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
* @returns {Promise<{
* deviceId: string,
* hasError: boolean,
@ -189,14 +191,14 @@ export function createLocalVideoTracks(ids: string[]) {
* label: string
* }[]>}
*/
export function createLocalAudioTracks(devices: Object[]) {
export function createLocalAudioTracks(devices: Object[], timeout: ?number) {
return Promise.all(
devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null;
let hasError = false;
try {
jitsiTrack = await createLocalTrack('audio', deviceId);
jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
} catch (err) {
hasError = true;
}

View File

@ -7,24 +7,22 @@ import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import { getMediaPermissionPromptVisibility } from '../../../overlay';
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
import { isAudioSettingsButtonDisabled } from '../../functions';
import AudioMuteButton from '../AudioMuteButton';
type Props = {
/**
* Indicates whether audio permissions have been granted or denied.
*/
hasPermissions: boolean,
/**
* Click handler for the small icon. Opens audio options.
*/
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.
*/
@ -34,83 +32,15 @@ type Props = {
* Flag controlling the visibility of the button.
* 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.
*
* @returns {ReactElement}
*/
class AudioSettingsButton extends Component<Props, State> {
_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;
}
class AudioSettingsButton extends Component<Props> {
/**
* Implements React's {@link Component#render}.
@ -118,8 +48,8 @@ class AudioSettingsButton extends Component<Props, State> {
* @inheritdoc
*/
render() {
const { isDisabled, onAudioOptionsClick, visible } = this.props;
const settingsDisabled = !this.state.hasPermissions
const { hasPermissions, isDisabled, onAudioOptionsClick, visible } = this.props;
const settingsDisabled = !hasPermissions
|| isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
@ -143,9 +73,11 @@ class AudioSettingsButton extends Component<Props, State> {
* @returns {Object}
*/
function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
return {
hasPermissions: permissions.audio,
isDisabled: isAudioSettingsButtonDisabled(state),
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
visible: !isMobileBrowser()
};
}

View File

@ -4,11 +4,9 @@ import React, { Component } from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
import { IconArrowDown } from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import { getLocalJitsiVideoTrack } from '../../../base/tracks';
import { getMediaPermissionPromptVisibility } from '../../../overlay';
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
import { isVideoSettingsButtonDisabled } from '../../functions';
import VideoMuteButton from '../VideoMuteButton';
@ -21,10 +19,9 @@ type Props = {
onVideoOptionsClick: Function,
/**
* Whether the permission prompt is visible or not.
* Useful for enabling the button on initial permission grant.
* Indicates whether video permissions have been granted or denied.
*/
permissionPromptVisibility: boolean,
hasPermissions: boolean,
/**
* 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
* camera at a time.
*/
visible: boolean,
};
type State = {
/**
* Whether the app has video permissions or not.
*/
hasPermissions: boolean,
visible: boolean
};
/**
@ -58,23 +47,7 @@ type State = {
*
* @returns {ReactElement}
*/
class VideoSettingsButton extends Component<Props, State> {
_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
};
}
class VideoSettingsButton extends Component<Props> {
/**
* Returns true if the settings icon is disabled.
@ -82,53 +55,9 @@ class VideoSettingsButton extends Component<Props, State> {
* @returns {boolean}
*/
_isIconDisabled() {
const { hasVideoTrack, isDisabled } = this.props;
const { hasPermissions, hasVideoTrack, isDisabled } = this.props;
return (!this.state.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;
return (!hasPermissions || isDisabled) && !hasVideoTrack;
}
/**
@ -159,10 +88,12 @@ class VideoSettingsButton extends Component<Props, State> {
* @returns {Object}
*/
function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
return {
hasPermissions: permissions.video,
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state),
permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
visible: !isMobileBrowser()
};
}