fix: Add GUM timeout & improve device permissions
This commit is contained in:
parent
7dc45c28a2
commit
a6c6cd6c56
|
@ -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 => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue