feat(device-selection) Separate Devices into Audio and Video in Settings (#12987)
Create separate tabs for Audio and Video in the Settings Dialog Move some settings from the More tab to Audio/ Video tab Implement redesign Convert some files to TS Move some styles from SCSS to JSS Enable device selection on welcome page
This commit is contained in:
parent
cfb8589bef
commit
0d0bec3aad
|
@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'reload_overlay/reload_overlay';
|
||||
@import 'mini_toolbox';
|
||||
@import 'modals/desktop-picker/desktop-picker';
|
||||
@import 'modals/device-selection/device-selection';
|
||||
@import 'modals/dialog';
|
||||
@import 'modals/embed-meeting/embed-meeting';
|
||||
@import 'modals/feedback/feedback';
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
.device-selection {
|
||||
.device-selectors {
|
||||
font-size: 14px;
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.device-selector-icon {
|
||||
align-self: center;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.device-selector-label {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
|
||||
.device-selector-trigger {
|
||||
background-color: #0E1624;
|
||||
border: 1px solid #455166;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
height: 2.3em;
|
||||
justify-content: space-between;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.device-selector-trigger-disabled {
|
||||
.device-selector-trigger {
|
||||
color: #a5adba;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selector-trigger-text {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-column {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&.column-selectors {
|
||||
margin-left: 15px;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.column-video {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-video-container {
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.video-input-preview {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
|
||||
> video {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.video-input-preview-error {
|
||||
color: $participantNameColor;
|
||||
display: none;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&.video-preview-has-error {
|
||||
background: black;
|
||||
|
||||
.video-input-preview-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.video-input-preview-display {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio-output-preview {
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
color: #6FB1EA;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #B3D4FF;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-input-preview {
|
||||
background: #1B2638;
|
||||
border-radius: 5px;
|
||||
height: 8px;
|
||||
|
||||
.audio-input-preview-level {
|
||||
background: #75B1FF;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
-webkit-transition: width .1s ease-in-out;
|
||||
-moz-transition: width .1s ease-in-out;
|
||||
-o-transition: width .1s ease-in-out;
|
||||
transition: width .1s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection.video-hidden {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.column-selectors {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.column-video {
|
||||
order: 1;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
|
@ -220,7 +220,7 @@
|
|||
"noPermission": "Permission not granted",
|
||||
"previewUnavailable": "Preview unavailable",
|
||||
"selectADevice": "Select a device",
|
||||
"testAudio": "Play a test sound"
|
||||
"testAudio": "Test"
|
||||
},
|
||||
"dialIn": {
|
||||
"screenTitle": "Dial-in summary"
|
||||
|
@ -971,6 +971,7 @@
|
|||
"title": "Security Options"
|
||||
},
|
||||
"settings": {
|
||||
"audio": "Audio",
|
||||
"buttonLabel": "Settings",
|
||||
"calendar": {
|
||||
"about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
|
||||
|
@ -1012,7 +1013,8 @@
|
|||
"startReactionsMuted": "Mute reaction sounds for everyone",
|
||||
"startVideoMuted": "Everyone starts hidden",
|
||||
"talkWhileMuted": "Talk while muted",
|
||||
"title": "Settings"
|
||||
"title": "Settings",
|
||||
"video": "Video"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Advanced",
|
||||
|
@ -1171,6 +1173,7 @@
|
|||
"download": "Download our apps",
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"embedMeeting": "Embed meeting",
|
||||
"enableNoiseSuppression": "Enable noise suppression",
|
||||
"endConference": "End meeting for all",
|
||||
"enterFullScreen": "View full screen",
|
||||
"enterTileView": "Enter tile view",
|
||||
|
|
|
@ -89,6 +89,10 @@ const useStyles = makeStyles()(theme => {
|
|||
}
|
||||
},
|
||||
|
||||
closeButtonContainer: {
|
||||
paddingBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
buttonContainer: {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
|
@ -138,20 +142,20 @@ interface IObject {
|
|||
[key: string]: string | string[] | boolean | number | number[] | {} | undefined;
|
||||
}
|
||||
|
||||
export interface IDialogTab {
|
||||
export interface IDialogTab<P> {
|
||||
className?: string;
|
||||
component: ComponentType<any>;
|
||||
icon: Function;
|
||||
labelKey: string;
|
||||
name: string;
|
||||
props?: IObject;
|
||||
propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
|
||||
propsUpdateFunction?: (tabState: IObject, newProps: P) => P;
|
||||
submit?: Function;
|
||||
}
|
||||
|
||||
interface IProps extends IBaseProps {
|
||||
defaultTab?: string;
|
||||
tabs: IDialogTab[];
|
||||
tabs: IDialogTab<any>[];
|
||||
}
|
||||
|
||||
const DialogWithTabs = ({
|
||||
|
@ -287,7 +291,7 @@ const DialogWithTabs = ({
|
|||
)}
|
||||
{(!isMobile || selectedTab) && (
|
||||
<div className = { classes.contentContainer }>
|
||||
<div className = { classes.buttonContainer }>
|
||||
<div className = { cx(classes.buttonContainer, classes.closeButtonContainer) }>
|
||||
{isMobile && (
|
||||
<span className = { classes.backContainer }>
|
||||
<ClickableIcon
|
||||
|
|
|
@ -79,7 +79,7 @@ const useStyles = makeStyles()(theme => {
|
|||
width: '100%',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
padding: '8px 16px',
|
||||
padding: '10px 16px',
|
||||
paddingRight: '42px',
|
||||
border: 0,
|
||||
appearance: 'none',
|
||||
|
|
|
@ -7,31 +7,23 @@ import {
|
|||
} from '../base/devices/actions';
|
||||
import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { toggleNoiseSuppression } from '../noise-suppression/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions';
|
||||
|
||||
import { getDeviceSelectionDialogProps } from './functions';
|
||||
import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Submits the settings related to device selection.
|
||||
* Submits the settings related to audio device selection.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId,
|
||||
userSelectedCameraDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
|
||||
}
|
||||
const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
|
||||
dispatch(updateSettings({
|
||||
|
@ -44,8 +36,8 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
|
|||
}
|
||||
|
||||
if (newState.selectedAudioOutputId
|
||||
&& newState.selectedAudioOutputId
|
||||
!== currentState.selectedAudioOutputId) {
|
||||
&& newState.selectedAudioOutputId
|
||||
!== currentState.selectedAudioOutputId) {
|
||||
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
|
||||
|
||||
setAudioOutputDeviceId(
|
||||
|
@ -62,5 +54,45 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
|
|||
err);
|
||||
});
|
||||
}
|
||||
|
||||
if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
|
||||
dispatch(toggleNoiseSuppression());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings related to device selection.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getVideoDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
|
||||
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: newState.selectedVideoInputId,
|
||||
userSelectedCameraDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
|
||||
}
|
||||
|
||||
if (newState.localFlipX !== currentState.localFlipX) {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: newState.localFlipX
|
||||
}));
|
||||
}
|
||||
|
||||
if (newState.currentFramerate !== currentState.currentFramerate) {
|
||||
const frameRate = parseInt(newState.currentFramerate, 10);
|
||||
|
||||
dispatch(setScreenshareFramerate(frameRate));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type IProps as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
import logger from '../logger';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceHidContainer from './DeviceHidContainer.web';
|
||||
import DeviceSelector from './DeviceSelector.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: {
|
||||
audioInput?: MediaDeviceInfo[];
|
||||
audioOutput?: MediaDeviceInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not the audio selector can be interacted with. If true,
|
||||
* the audio input selector will be rendered as disabled. This is
|
||||
* specifically used to prevent audio device changing in Firefox, which
|
||||
* currently does not work due to a browser-side regression.
|
||||
*/
|
||||
disableAudioInputChange: boolean;
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasAudioPermission: boolean;
|
||||
|
||||
/**
|
||||
* If true, the audio meter will not display. Necessary for browsers or
|
||||
* configurations that do not support local stats to prevent a
|
||||
* non-responsive mic preview from displaying.
|
||||
*/
|
||||
hideAudioInputPreview: boolean;
|
||||
|
||||
/**
|
||||
* If true, the button to play a test sound on the selected speaker will not be displayed.
|
||||
* This needs to be hidden on browsers that do not support selecting an audio output device.
|
||||
*/
|
||||
hideAudioOutputPreview: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the audio output source selector should display. If
|
||||
* true, the audio output selector and test audio link will not be
|
||||
* rendered.
|
||||
*/
|
||||
hideAudioOutputSelect: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the hid device container should display.
|
||||
*/
|
||||
hideDeviceHIDContainer: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide noise suppression checkbox or not.
|
||||
*/
|
||||
hideNoiseSuppression: boolean;
|
||||
|
||||
/**
|
||||
* Wether noise suppression is on or not.
|
||||
*/
|
||||
noiseSuppressionEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
selectedAudioInputId: string;
|
||||
|
||||
/**
|
||||
* The id of the audio output device to preview.
|
||||
*/
|
||||
selectedAudioOutputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
previewAudioTrack?: any | null;
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '0 2px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
inputContainer: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
outputContainer: {
|
||||
margin: `${theme.spacing(5)} 0`,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
|
||||
outputButton: {
|
||||
marginLeft: theme.spacing(3)
|
||||
},
|
||||
|
||||
noiseSuppressionContainer: {
|
||||
marginBottom: theme.spacing(5)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AudioDevicesSelection extends AbstractDialogTab<IProps, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewAudioTrack: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => {
|
||||
this.props.dispatch(getAvailableDevices());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps.selectedAudioInputId
|
||||
!== this.props.selectedAudioInputId) {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeAudioInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
hasAudioPermission,
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputPreview,
|
||||
hideDeviceHIDContainer,
|
||||
hideNoiseSuppression,
|
||||
noiseSuppressionEnabled,
|
||||
selectedAudioOutputId,
|
||||
t
|
||||
} = this.props;
|
||||
const { audioInput, audioOutput } = this._getSelectors();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
aria-live = 'polite'
|
||||
className = { classes.inputContainer }>
|
||||
{this._renderSelector(audioInput)}
|
||||
</div>
|
||||
{!hideAudioInputPreview && hasAudioPermission
|
||||
&& <AudioInputPreview
|
||||
track = { this.state.previewAudioTrack } />}
|
||||
<div
|
||||
aria-live = 'polite'
|
||||
className = { classes.outputContainer }>
|
||||
{this._renderSelector(audioOutput)}
|
||||
{!hideAudioOutputPreview && hasAudioPermission
|
||||
&& <AudioOutputPreview
|
||||
className = { classes.outputButton }
|
||||
deviceId = { selectedAudioOutputId } />}
|
||||
</div>
|
||||
{!hideNoiseSuppression && (
|
||||
<div className = { classes.noiseSuppressionContainer }>
|
||||
<Checkbox
|
||||
checked = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.enableNoiseSuppression') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({
|
||||
noiseSuppressionEnabled: !noiseSuppressionEnabled
|
||||
}) } />
|
||||
</div>
|
||||
)}
|
||||
{!hideDeviceHIDContainer
|
||||
&& <DeviceHidContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitsiTrack for the audio input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of audio input device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId: string) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current audio input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeAudioInputPreview(): Promise<any> {
|
||||
return this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSelector(deviceSelectorProps: any) {
|
||||
return deviceSelectorProps ? (
|
||||
<DeviceSelector
|
||||
{ ...deviceSelectorProps }
|
||||
key = { deviceSelectorProps.id } />
|
||||
) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object configurations for audio input and output.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} Configurations.
|
||||
*/
|
||||
_getSelectors() {
|
||||
const { availableDevices, hasAudioPermission } = this.props;
|
||||
|
||||
const audioInput = {
|
||||
devices: availableDevices.audioInput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
id: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
};
|
||||
let audioOutput;
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
audioOutput = {
|
||||
devices: availableDevices.audioOutput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-speaker',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
id: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: (selectedAudioOutputId: string) => super._onChange({ selectedAudioOutputId }),
|
||||
selectedDeviceId: this.props.selectedAudioOutputId
|
||||
};
|
||||
}
|
||||
|
||||
return { audioInput,
|
||||
audioOutput };
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices ?? {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(translate(AudioDevicesSelection)));
|
|
@ -1,150 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to show an audio level meter for.
|
||||
*/
|
||||
track: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current audio input level being received, from 0 to 1.
|
||||
*/
|
||||
audioLevel: number
|
||||
};
|
||||
|
||||
/**
|
||||
* React component for displaying a audio level meter for a JitsiLocalTrack.
|
||||
*/
|
||||
class AudioInputPreview extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new AudioInputPreview instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioLevel: 0
|
||||
};
|
||||
|
||||
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates after the initial render.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._listenForAudioUpdates(this.props.track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for audio level updates on the old track and starts
|
||||
* listening instead on the new track.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.track !== this.props.track) {
|
||||
this._listenForAudioUpdates(this.props.track);
|
||||
this._updateAudioLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from audio level updates.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._stopListeningForAudioUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const audioMeterFill = {
|
||||
width: `${Math.floor(this.state.audioLevel * 100)}%`
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = 'audio-input-preview' >
|
||||
<div
|
||||
className = 'audio-input-preview-level'
|
||||
style = { audioMeterFill } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates from the library.
|
||||
*
|
||||
* @param {JitstiLocalTrack} track - The track to listen to for audio level
|
||||
* updates.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_listenForAudioUpdates(track) {
|
||||
this._stopListeningForAudioUpdates();
|
||||
|
||||
track && track.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateAudioLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to further updates from the current track.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopListeningForAudioUpdates() {
|
||||
this.props.track && this.props.track.off(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateAudioLevel);
|
||||
}
|
||||
|
||||
_updateAudioLevel: (number) => void;
|
||||
|
||||
/**
|
||||
* Updates the internal state of the last know audio level. The level should
|
||||
* be between 0 and 1, as the level will be used as a percentage out of 1.
|
||||
*
|
||||
* @param {number} audioLevel - The new audio level for the track.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAudioLevel(audioLevel) {
|
||||
this.setState({
|
||||
audioLevel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioInputPreview;
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
// @ts-ignore
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet/_.web';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioInputPreview}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to show an audio level meter for.
|
||||
*/
|
||||
track: any;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
section: {
|
||||
flex: 1,
|
||||
height: '4px',
|
||||
borderRadius: '1px',
|
||||
backgroundColor: theme.palette.ui04,
|
||||
marginRight: theme.spacing(1),
|
||||
|
||||
'&:last-of-type': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
|
||||
activeSection: {
|
||||
backgroundColor: theme.palette.success01
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const NO_OF_PREVIEW_SECTIONS = 11;
|
||||
|
||||
const AudioInputPreview = (props: IProps) => {
|
||||
const [ audioLevel, setAudioLevel ] = useState(0);
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates from the library.
|
||||
*
|
||||
* @param {JitsiLocalTrack} track - The track to listen to for audio level
|
||||
* updates.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _listenForAudioUpdates(track: any) {
|
||||
_stopListeningForAudioUpdates();
|
||||
|
||||
track?.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
setAudioLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to further updates from the current track.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _stopListeningForAudioUpdates() {
|
||||
props.track?.off(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
setAudioLevel);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
_listenForAudioUpdates(props.track);
|
||||
|
||||
return _stopListeningForAudioUpdates;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
_listenForAudioUpdates(props.track);
|
||||
setAudioLevel(0);
|
||||
}, [ props.track ]);
|
||||
|
||||
const audioMeterFill = Math.ceil(Math.floor(audioLevel * 100) / (100 / NO_OF_PREVIEW_SECTIONS));
|
||||
|
||||
return (
|
||||
<div className = { classes.container } >
|
||||
{new Array(NO_OF_PREVIEW_SECTIONS).fill(0)
|
||||
.map((_, idx) =>
|
||||
(<div
|
||||
className = { cx(classes.section, idx < audioMeterFill && classes.activeSection) }
|
||||
key = { idx } />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioInputPreview;
|
|
@ -1,35 +1,38 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Audio from '../../base/media/components/Audio';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import Audio from '../../base/media/components/Audio.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.any';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.mp3';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioOutputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Button className.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The device id of the audio output device to use.
|
||||
*/
|
||||
deviceId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component for playing a test sound through a specified audio device.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AudioOutputPreview extends Component<Props> {
|
||||
_audioElement: ?Object;
|
||||
class AudioOutputPreview extends Component<IProps> {
|
||||
_audioElement: HTMLAudioElement | null;
|
||||
|
||||
/**
|
||||
* Initializes a new AudioOutputPreview instance.
|
||||
|
@ -37,7 +40,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._audioElement = null;
|
||||
|
@ -66,24 +69,21 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className = 'audio-output-preview'>
|
||||
<a
|
||||
aria-label = { this.props.t('deviceSelection.testAudio') }
|
||||
<>
|
||||
<Button
|
||||
accessibilityLabel = { this.props.t('deviceSelection.testAudio') }
|
||||
className = { this.props.className }
|
||||
labelKey = 'deviceSelection.testAudio'
|
||||
onClick = { this._onClick }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ this.props.t('deviceSelection.testAudio') }
|
||||
</a>
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
<Audio
|
||||
setRef = { this._audioElementReady }
|
||||
src = { TEST_SOUND_PATH } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
_audioElementReady: (Object) => void;
|
||||
|
||||
/**
|
||||
* Sets the instance variable for the component's audio element so it can be
|
||||
* accessed directly.
|
||||
|
@ -92,14 +92,12 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_audioElementReady(element: Object) {
|
||||
_audioElementReady(element: HTMLAudioElement) {
|
||||
this._audioElement = element;
|
||||
|
||||
this._setAudioSink();
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* Plays a test sound.
|
||||
*
|
||||
|
@ -107,12 +105,9 @@ class AudioOutputPreview extends Component<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._audioElement
|
||||
&& this._audioElement.play();
|
||||
this._audioElement?.play();
|
||||
}
|
||||
|
||||
_onKeyPress: (Object) => void;
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
|
@ -120,7 +115,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e) {
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClick();
|
||||
|
@ -135,7 +130,7 @@ class AudioOutputPreview extends Component<Props> {
|
|||
*/
|
||||
_setAudioSink() {
|
||||
this._audioElement
|
||||
&& this.props.deviceId
|
||||
&& this.props.deviceId // @ts-ignore
|
||||
&& this._audioElement.setSinkId(this.props.deviceId);
|
||||
}
|
||||
}
|
|
@ -5,33 +5,40 @@ import { makeStyles } from 'tss-react/mui';
|
|||
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconTrash } from '../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.any';
|
||||
import { closeHidDevice, requestHidDevice } from '../../web-hid/actions';
|
||||
import { getDeviceInfo, shouldRequestHIDDevice } from '../../web-hid/functions';
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
callControlContainer: {
|
||||
marginTop: '8px',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
'> label': {
|
||||
display: 'block',
|
||||
marginBottom: '20px'
|
||||
}
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
|
||||
label: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
|
||||
deviceRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
deleteDevice: {
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
headerConnectedDevice: {
|
||||
fontWeight: 600
|
||||
},
|
||||
|
||||
hidContainer: {
|
||||
'> span': {
|
||||
marginLeft: '16px'
|
||||
|
@ -66,7 +73,7 @@ function DeviceHidContainer() {
|
|||
className = { classes.callControlContainer }
|
||||
key = 'callControl'>
|
||||
<label
|
||||
className = 'device-selector-label'
|
||||
className = { classes.label }
|
||||
htmlFor = 'callControl'>
|
||||
{t('deviceSelection.hid.callControl')}
|
||||
</label>
|
||||
|
@ -77,7 +84,6 @@ function DeviceHidContainer() {
|
|||
key = 'request-control-btn'
|
||||
label = { t('deviceSelection.hid.pairDevice') }
|
||||
onClick = { onRequestControl }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
)}
|
||||
{!showRequestDeviceInfo && (
|
||||
|
|
|
@ -1,429 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type Props as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceHidContainer from './DeviceHidContainer.web';
|
||||
import DeviceSelector from './DeviceSelector';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DeviceSelection}.
|
||||
*/
|
||||
export type Props = {
|
||||
...$Exact<AbstractDialogTabProps>,
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: Object,
|
||||
|
||||
/**
|
||||
* Whether or not the audio selector can be interacted with. If true,
|
||||
* the audio input selector will be rendered as disabled. This is
|
||||
* specifically used to prevent audio device changing in Firefox, which
|
||||
* currently does not work due to a browser-side regression.
|
||||
*/
|
||||
disableAudioInputChange: boolean,
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean,
|
||||
|
||||
/**
|
||||
* Whether video input dropdown should be enabled or not.
|
||||
*/
|
||||
disableVideoInputSelect: 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
|
||||
* non-responsive mic preview from displaying.
|
||||
*/
|
||||
hideAudioInputPreview: boolean,
|
||||
|
||||
/**
|
||||
* If true, the button to play a test sound on the selected speaker will not be displayed.
|
||||
* This needs to be hidden on browsers that do not support selecting an audio output device.
|
||||
*/
|
||||
hideAudioOutputPreview: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the audio output source selector should display. If
|
||||
* true, the audio output selector and test audio link will not be
|
||||
* rendered.
|
||||
*/
|
||||
hideAudioOutputSelect: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the hid device container should display.
|
||||
*/
|
||||
hideDeviceHIDContainer: boolean,
|
||||
|
||||
/**
|
||||
* Whether video input preview should be displayed or not.
|
||||
* (In the case of iOS Safari).
|
||||
*/
|
||||
hideVideoInputPreview: boolean,
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
selectedAudioInputId: string,
|
||||
|
||||
/**
|
||||
* The id of the audio output device to preview.
|
||||
*/
|
||||
selectedAudioOutputId: string,
|
||||
|
||||
/**
|
||||
* The id of the video input device to preview.
|
||||
*/
|
||||
selectedVideoInputId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link DeviceSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
previewAudioTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing video input.
|
||||
*/
|
||||
previewVideoTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The error message from trying to use a video input device.
|
||||
*/
|
||||
previewVideoTrackError: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewAudioTrack: null,
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId),
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => getAvailableDevices());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedAudioInputId
|
||||
!== this.props.selectedAudioInputId) {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
}
|
||||
|
||||
if (prevProps.selectedVideoInputId
|
||||
!== this.props.selectedVideoInputId) {
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeAudioInputPreview();
|
||||
this._disposeVideoInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputPreview,
|
||||
hideDeviceHIDContainer,
|
||||
hideVideoInputPreview,
|
||||
selectedAudioOutputId
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
|
||||
<div className = 'device-selection-column column-video'>
|
||||
{ !hideVideoInputPreview
|
||||
&& <div className = 'device-selection-video-container'>
|
||||
<VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
</div>
|
||||
}
|
||||
{ !hideAudioInputPreview
|
||||
&& <AudioInputPreview
|
||||
track = { this.state.previewAudioTrack } /> }
|
||||
</div>
|
||||
<div className = 'device-selection-column column-selectors'>
|
||||
<div
|
||||
aria-live = 'polite all'
|
||||
className = 'device-selectors'>
|
||||
{ this._renderSelectors() }
|
||||
</div>
|
||||
{ !hideAudioOutputPreview
|
||||
&& <AudioOutputPreview
|
||||
deviceId = { selectedAudioOutputId } /> }
|
||||
{ !hideDeviceHIDContainer
|
||||
&& <DeviceHidContainer /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitiTrack for the audio input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of audio input device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitiTrack for the video input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of video device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createVideoInputTrack(deviceId) {
|
||||
const { hideVideoInputPreview } = this.props;
|
||||
|
||||
if (hideVideoInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current audio input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeAudioInputPreview(): Promise<*> {
|
||||
return this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current video input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeVideoInputPreview(): Promise<*> {
|
||||
return this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSelector(deviceSelectorProps) {
|
||||
return (
|
||||
<div key = { deviceSelectorProps.label }>
|
||||
<label
|
||||
className = 'device-selector-label'
|
||||
htmlFor = { deviceSelectorProps.id }>
|
||||
{ this.props.t(deviceSelectorProps.label) }
|
||||
</label>
|
||||
<DeviceSelector { ...deviceSelectorProps } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DeviceSelector instances for video output, audio input, and audio
|
||||
* output.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<ReactElement>} DeviceSelector instances.
|
||||
*/
|
||||
_renderSelectors() {
|
||||
const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
|
||||
|
||||
const configurations = [
|
||||
{
|
||||
devices: availableDevices.audioInput,
|
||||
hasPermission: hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
id: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
},
|
||||
{
|
||||
devices: availableDevices.videoInput,
|
||||
hasPermission: hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
id: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
|
||||
selectedDeviceId: this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
configurations.push({
|
||||
devices: availableDevices.audioOutput,
|
||||
hasPermission: hasAudioPermission || hasVideoPermission,
|
||||
icon: 'icon-speaker',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
id: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
|
||||
selectedDeviceId: this.props.selectedAudioOutputId
|
||||
});
|
||||
}
|
||||
|
||||
return configurations.map(config => this._renderSelector(config));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(DeviceSelection);
|
|
@ -1,58 +1,76 @@
|
|||
/* @flow */
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Select from '../../base/ui/components/web/Select';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DeviceSelector}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* MediaDeviceInfos used for display in the select element.
|
||||
*/
|
||||
devices: Array<Object>,
|
||||
devices: Array<MediaDeviceInfo> | undefined;
|
||||
|
||||
/**
|
||||
* If false, will return a selector with no selection options.
|
||||
*/
|
||||
hasPermission: boolean,
|
||||
hasPermission: boolean;
|
||||
|
||||
/**
|
||||
* CSS class for the icon to the left of the dropdown trigger.
|
||||
*/
|
||||
icon: string,
|
||||
|
||||
/**
|
||||
* If true, will render the selector disabled with a default selection.
|
||||
*/
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The translation key to display as a menu label.
|
||||
*/
|
||||
label: string,
|
||||
|
||||
/**
|
||||
* The callback to invoke when a selection is made.
|
||||
*/
|
||||
onSelect: Function,
|
||||
|
||||
/**
|
||||
* The default device to display as selected.
|
||||
*/
|
||||
selectedDeviceId: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* The id of the dropdown element.
|
||||
*/
|
||||
id: string
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* If true, will render the selector disabled with a default selection.
|
||||
*/
|
||||
isDisabled: boolean;
|
||||
|
||||
/**
|
||||
* The translation key to display as a menu label.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The callback to invoke when a selection is made.
|
||||
*/
|
||||
onSelect: Function;
|
||||
|
||||
/**
|
||||
* The default device to display as selected.
|
||||
*/
|
||||
selectedDeviceId: string;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
textSelector: {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
padding: '10px 16px',
|
||||
textAlign: 'center',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
border: `1px solid ${theme.palette.ui03}`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -61,17 +79,18 @@ type Props = {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DeviceSelector extends Component<Props> {
|
||||
class DeviceSelector extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new DeviceSelector instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onSelect = this._onSelect.bind(this);
|
||||
this._createDropdown = this._createDropdown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,25 +130,6 @@ class DeviceSelector extends Component<Props> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a React Element for displaying the passed in text surrounded by
|
||||
* two icons. The left icon is the icon class passed in through props and
|
||||
* the right icon is AtlasKit ExpandIcon.
|
||||
*
|
||||
* @param {string} triggerText - The text to display within the element.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdownTrigger(triggerText) {
|
||||
return (
|
||||
<div className = 'device-selector-trigger'>
|
||||
<span className = 'device-selector-trigger-text'>
|
||||
{ triggerText }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a AKDropdownMenu Component using passed in props and options. If
|
||||
* the dropdown needs to be disabled, then only the AKDropdownMenu trigger
|
||||
|
@ -146,32 +146,30 @@ class DeviceSelector extends Component<Props> {
|
|||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdown(options) {
|
||||
_createDropdown(options: { defaultSelected?: MediaDeviceInfo; isDisabled: boolean;
|
||||
items?: Array<{ label: string; value: string; }>; placeholder: string; }) {
|
||||
const triggerText
|
||||
= (options.defaultSelected && (options.defaultSelected.label || options.defaultSelected.deviceId))
|
||||
|| options.placeholder;
|
||||
const trigger = this._createDropdownTrigger(triggerText);
|
||||
const { classes } = this.props;
|
||||
|
||||
if (options.isDisabled || !options.items.length) {
|
||||
if (options.isDisabled || !options.items?.length) {
|
||||
return (
|
||||
<div className = 'device-selector-trigger-disabled'>
|
||||
{ trigger }
|
||||
<div className = { classes.textSelector }>
|
||||
{triggerText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
onChange = { this._onSelect }
|
||||
options = { options.items }
|
||||
value = { this.props.selectedDeviceId } />
|
||||
</div>
|
||||
<Select
|
||||
label = { this.props.t(this.props.label) }
|
||||
onChange = { this._onSelect }
|
||||
options = { options.items }
|
||||
value = { this.props.selectedDeviceId } />
|
||||
);
|
||||
}
|
||||
|
||||
_onSelect: (Object) => void;
|
||||
|
||||
/**
|
||||
* Invokes the passed in callback to notify of selection changes.
|
||||
*
|
||||
|
@ -180,7 +178,7 @@ class DeviceSelector extends Component<Props> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelect(e) {
|
||||
_onSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const deviceId = e.target.value;
|
||||
|
||||
if (this.props.selectedDeviceId !== deviceId) {
|
||||
|
@ -217,4 +215,4 @@ class DeviceSelector extends Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default translate(DeviceSelector);
|
||||
export default withStyles(styles)(translate(DeviceSelector));
|
|
@ -0,0 +1,368 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type IProps as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
import Select from '../../base/ui/components/web/Select';
|
||||
import { SS_DEFAULT_FRAME_RATE } from '../../settings/constants';
|
||||
import logger from '../logger';
|
||||
|
||||
import DeviceSelector from './DeviceSelector.web';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoDeviceSelection}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: { videoInput?: MediaDeviceInfo[]; };
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* The currently selected desktop share frame rate in the frame rate select dropdown.
|
||||
*/
|
||||
currentFramerate: string;
|
||||
|
||||
/**
|
||||
* All available desktop capture frame rates.
|
||||
*/
|
||||
desktopShareFramerates: Array<number>;
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: boolean;
|
||||
|
||||
/**
|
||||
* Whether video input dropdown should be enabled or not.
|
||||
*/
|
||||
disableVideoInputSelect: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the audio permission was granted.
|
||||
*/
|
||||
hasVideoPermission: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide the additional settings or not.
|
||||
*/
|
||||
hideAdditionalSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether video input preview should be displayed or not.
|
||||
* (In the case of iOS Safari).
|
||||
*/
|
||||
hideVideoInputPreview: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local video is flipped.
|
||||
*/
|
||||
localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* The id of the video input device to preview.
|
||||
*/
|
||||
selectedVideoInputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VideoDeviceSelection}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing video input.
|
||||
*/
|
||||
previewVideoTrack: any | null;
|
||||
|
||||
/**
|
||||
* The error message from trying to use a video input device.
|
||||
*/
|
||||
previewVideoTrackError: string | null;
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '0 2px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
checkboxContainer: {
|
||||
margin: `${theme.spacing(4)} 0`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoDeviceSelection extends AbstractDialogTab<IProps, State> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
*
|
||||
* In component did mount we start a Promise to create tracks and
|
||||
* set the tracks in the state, if we unmount the component in the meanwhile
|
||||
* tracks will be created and will never been disposed (dispose tracks is
|
||||
* in componentWillUnmount). When tracks are created and component is
|
||||
* unmounted we dispose the tracks.
|
||||
*/
|
||||
_unMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelection instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
|
||||
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial previews for audio input and video input.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._unMounted = false;
|
||||
Promise.all([
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => {
|
||||
this.props.dispatch(getAvailableDevices());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if audio / video permissions were granted. Updates audio input and
|
||||
* video input previews.
|
||||
*
|
||||
* @param {Object} prevProps - Previous props this component received.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
|
||||
if (prevProps.selectedVideoInputId
|
||||
!== this.props.selectedVideoInputId) {
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure preview tracks are destroyed to prevent continued use.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeVideoInputPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
hideAdditionalSettings,
|
||||
hideVideoInputPreview,
|
||||
localFlipX,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{ !hideVideoInputPreview
|
||||
&& <VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
localFlipX = { localFlipX }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
}
|
||||
<div
|
||||
aria-live = 'polite'>
|
||||
{this._renderVideoSelector()}
|
||||
</div>
|
||||
{!hideAdditionalSettings && (
|
||||
<>
|
||||
<div className = { classes.checkboxContainer }>
|
||||
<Checkbox
|
||||
checked = { localFlipX }
|
||||
label = { t('videothumbnail.mirrorVideo') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({ localFlipX: !localFlipX }) } />
|
||||
</div>
|
||||
{this._renderFramerateSelect()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the JitsiTrack for the video input preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of video device to preview.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_createVideoInputTrack(deviceId: string) {
|
||||
const { hideVideoInputPreview } = this.props;
|
||||
|
||||
if (hideVideoInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (this._unMounted) {
|
||||
jitsiLocalTrack.dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current video input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeVideoInputPreview(): Promise<any> {
|
||||
return this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoSelector() {
|
||||
const { availableDevices, hasVideoPermission } = this.props;
|
||||
|
||||
const videoConfig = {
|
||||
devices: availableDevices.videoInput,
|
||||
hasPermission: hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
id: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: (selectedVideoInputId: string) => super._onChange({ selectedVideoInputId }),
|
||||
selectedDeviceId: this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
|
||||
};
|
||||
|
||||
return (
|
||||
<DeviceSelector
|
||||
{ ...videoConfig }
|
||||
key = { videoConfig.id } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a frame rate from the select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const frameRate = e.target.value;
|
||||
|
||||
super._onChange({ currentFramerate: frameRate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for the desktop share frame rate dropdown.
|
||||
*
|
||||
* @returns {JSX}
|
||||
*/
|
||||
_renderFramerateSelect() {
|
||||
const { currentFramerate, desktopShareFramerates, t } = this.props;
|
||||
const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
|
||||
return {
|
||||
value: frameRate,
|
||||
label: `${frameRate} ${t('settings.framesPerSecond')}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
|
||||
? t('settings.desktopShareHighFpsWarning')
|
||||
: t('settings.desktopShareWarning') }
|
||||
label = { t('settings.desktopShareFramerate') }
|
||||
onChange = { this._onFramerateItemSelect }
|
||||
options = { frameRateItems }
|
||||
value = { currentFramerate } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices ?? {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(translate(VideoDeviceSelection)));
|
|
@ -1,58 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Video from '../../base/media/components/Video';
|
||||
|
||||
const VIDEO_ERROR_CLASS = 'video-preview-has-error';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoInputPreview}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* An error message to display instead of a preview. Displaying an error
|
||||
* will take priority over displaying a video preview.
|
||||
*/
|
||||
error: ?string,
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
track: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* React component for displaying video. This component defers to lib-jitsi-meet
|
||||
* logic for rendering the video.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoInputPreview extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { error } = this.props;
|
||||
const errorClass = error ? VIDEO_ERROR_CLASS : '';
|
||||
const className = `video-input-preview ${errorClass}`;
|
||||
|
||||
return (
|
||||
<div className = { className }>
|
||||
<Video
|
||||
className = 'video-input-preview-display flipVideoX'
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: this.props.track }} />
|
||||
<div className = 'video-input-preview-error'>
|
||||
{ error || '' }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoInputPreview;
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Video from '../../base/media/components/Video.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoInputPreview}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* An error message to display instead of a preview. Displaying an error
|
||||
* will take priority over displaying a video preview.
|
||||
*/
|
||||
error: string | null;
|
||||
|
||||
/**
|
||||
* Whether or not the local video is flipped.
|
||||
*/
|
||||
localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
track: Object;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
marginBottom: theme.spacing(4),
|
||||
backgroundColor: theme.palette.uiBackground
|
||||
},
|
||||
|
||||
video: {
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
errorText: {
|
||||
color: theme.palette.text01,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
top: '50%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const VideoInputPreview = ({ error, localFlipX, track }: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<Video
|
||||
className = { cx(classes.video, localFlipX && 'flipVideoX') }
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: track }} />
|
||||
{error && (
|
||||
<div className = { classes.errorText }>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoInputPreview;
|
|
@ -1,4 +0,0 @@
|
|||
// @flow
|
||||
|
||||
export { default as DeviceSelection } from './DeviceSelection';
|
||||
export type { Props as DeviceSelectionProps } from './DeviceSelection';
|
|
@ -21,8 +21,69 @@ import {
|
|||
getUserSelectedMicDeviceId,
|
||||
getUserSelectedOutputDeviceId
|
||||
} from '../base/settings/functions.web';
|
||||
import { isNoiseSuppressionEnabled } from '../noise-suppression/functions';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from '../settings/constants';
|
||||
import { isDeviceHidSupported } from '../web-hid/functions';
|
||||
|
||||
/**
|
||||
* Returns the properties for the audio device selection dialog from Redux state.
|
||||
*
|
||||
* @param {IStateful} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the audio device selection dialog.
|
||||
*/
|
||||
export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
|
||||
// by the browser when a new track is created for preview. That's why we are disabling all previews.
|
||||
const disablePreviews = isIosMobileBrowser();
|
||||
|
||||
const state = toState(stateful);
|
||||
const settings = state['features/base/settings'];
|
||||
const { permissions } = state['features/base/devices'];
|
||||
const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
|
||||
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
|
||||
const userSelectedMic = getUserSelectedMicDeviceId(state);
|
||||
const deviceHidSupported = isDeviceHidSupported();
|
||||
const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
|
||||
const hideNoiseSuppression = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
|
||||
|
||||
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
|
||||
// case for Safari on iOS.
|
||||
let disableAudioInputChange
|
||||
= !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
|
||||
let selectedAudioInputId = settings.micDeviceId;
|
||||
let selectedAudioOutputId = getAudioOutputDeviceId();
|
||||
|
||||
// audio input change will be a problem only when we are in a
|
||||
// conference and this is not supported, when we open device selection on
|
||||
// welcome page changing input devices will not be a problem
|
||||
// on welcome page we also show only what we have saved as user selected devices
|
||||
if (isDisplayedOnWelcomePage) {
|
||||
disableAudioInputChange = false;
|
||||
selectedAudioInputId = userSelectedMic;
|
||||
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
|
||||
}
|
||||
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
// used or if none are currently used with what we have in settings(user selected)
|
||||
return {
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
hasAudioPermission: permissions.audio,
|
||||
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
|
||||
hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
|
||||
hideAudioOutputSelect: !speakerChangeSupported,
|
||||
hideDeviceHIDContainer: !deviceHidSupported,
|
||||
hideNoiseSuppression,
|
||||
noiseSuppressionEnabled,
|
||||
selectedAudioInputId,
|
||||
selectedAudioOutputId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the device selection dialog from Redux state.
|
||||
*
|
||||
|
@ -32,7 +93,7 @@ import { isDeviceHidSupported } from '../web-hid/functions';
|
|||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the device selection dialog.
|
||||
*/
|
||||
export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
|
||||
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
|
||||
// by the browser when a new track is created for preview. That's why we are disabling all previews.
|
||||
const disablePreviews = isIosMobileBrowser();
|
||||
|
@ -41,18 +102,12 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
|
|||
const settings = state['features/base/settings'];
|
||||
const { permissions } = state['features/base/devices'];
|
||||
const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
|
||||
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
|
||||
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
|
||||
const userSelectedMic = getUserSelectedMicDeviceId(state);
|
||||
const deviceHidSupported = isDeviceHidSupported();
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const hideAdditionalSettings = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
|
||||
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
|
||||
|
||||
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
|
||||
// case for Safari on iOS.
|
||||
let disableAudioInputChange
|
||||
= !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
|
||||
let disableVideoInputSelect = !inputDeviceChangeSupported;
|
||||
let selectedAudioInputId = settings.micDeviceId;
|
||||
let selectedAudioOutputId = getAudioOutputDeviceId();
|
||||
let selectedVideoInputId = settings.cameraDeviceId;
|
||||
|
||||
// audio input change will be a problem only when we are in a
|
||||
|
@ -60,29 +115,21 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
|
|||
// welcome page changing input devices will not be a problem
|
||||
// on welcome page we also show only what we have saved as user selected devices
|
||||
if (isDisplayedOnWelcomePage) {
|
||||
disableAudioInputChange = false;
|
||||
disableVideoInputSelect = false;
|
||||
selectedAudioInputId = userSelectedMic;
|
||||
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
|
||||
selectedVideoInputId = userSelectedCamera;
|
||||
}
|
||||
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
// used or if none are currently used with what we have in settings(user selected)
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices,
|
||||
disableAudioInputChange,
|
||||
currentFramerate: framerate,
|
||||
desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
|
||||
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
disableVideoInputSelect,
|
||||
hasAudioPermission: permissions.audio,
|
||||
hasVideoPermission: permissions.video,
|
||||
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
|
||||
hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
|
||||
hideAudioOutputSelect: !speakerChangeSupported,
|
||||
hideDeviceHIDContainer: !deviceHidSupported,
|
||||
hideAdditionalSettings,
|
||||
hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
|
||||
selectedAudioInputId,
|
||||
selectedAudioOutputId,
|
||||
localFlipX: Boolean(localFlipX),
|
||||
selectedVideoInputId
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './actions.any';
|
|
@ -11,7 +11,6 @@ import {
|
|||
import { openDialog } from '../base/dialog/actions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
|
@ -99,12 +98,6 @@ export function submitMoreTab(newState: any) {
|
|||
}));
|
||||
}
|
||||
|
||||
if (newState.currentFramerate !== currentState.currentFramerate) {
|
||||
const frameRate = parseInt(newState.currentFramerate, 10);
|
||||
|
||||
dispatch(setScreenshareFramerate(frameRate));
|
||||
}
|
||||
|
||||
if (newState.maxStageParticipants !== currentState.maxStageParticipants) {
|
||||
dispatch(updateSettings({ maxStageParticipants: Number(newState.maxStageParticipants) }));
|
||||
}
|
||||
|
|
|
@ -8,23 +8,12 @@ import { translate } from '../../../base/i18n/functions';
|
|||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip/constants';
|
||||
import { SS_DEFAULT_FRAME_RATE } from '../../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link MoreTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
|
||||
/**
|
||||
* The currently selected desktop share frame rate in the frame rate select dropdown.
|
||||
*/
|
||||
currentFramerate: string;
|
||||
|
||||
/**
|
||||
* All available desktop capture frame rates.
|
||||
*/
|
||||
desktopShareFramerates: Array<number>;
|
||||
|
||||
/**
|
||||
* Whether or not follow me is currently active (enabled by some other participant).
|
||||
*/
|
||||
|
@ -77,7 +66,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
|
||||
this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
|
||||
this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
|
||||
this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
|
||||
|
@ -104,19 +92,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a frame rate from the select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const frameRate = e.target.value;
|
||||
|
||||
super._onChange({ currentFramerate: frameRate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if the lobby
|
||||
* should be shown.
|
||||
|
@ -142,38 +117,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
super._onChange({ maxStageParticipants: maxParticipants });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for the desktop share frame rate dropdown.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFramerateSelect() {
|
||||
const { currentFramerate, desktopShareFramerates, t } = this.props;
|
||||
const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
|
||||
return {
|
||||
value: frameRate,
|
||||
label: `${frameRate} ${t('settings.framesPerSecond')}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'frameRate'>
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
|
||||
? t('settings.desktopShareHighFpsWarning')
|
||||
: t('settings.desktopShareWarning') }
|
||||
label = { t('settings.desktopShareFramerate') }
|
||||
onChange = { this._onFramerateItemSelect }
|
||||
options = { frameRateItems }
|
||||
value = { currentFramerate } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying prejoin screen settings.
|
||||
*
|
||||
|
@ -244,7 +187,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
<div
|
||||
className = 'settings-sub-pane right'
|
||||
key = 'settings-sub-pane-right'>
|
||||
{ this._renderFramerateSelect() }
|
||||
{ this._renderMaxStageParticipantsSelect() }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -46,7 +46,7 @@ class SettingsButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { defaultTab = SETTINGS_TABS.DEVICES, dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
const { defaultTab = SETTINGS_TABS.AUDIO, dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('settings'));
|
||||
dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -11,18 +10,20 @@ import {
|
|||
IconHost,
|
||||
IconShortcuts,
|
||||
IconUser,
|
||||
IconVideo,
|
||||
IconVolumeUp
|
||||
} from '../../../base/icons/svg';
|
||||
import { connect } from '../../../base/redux/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions.web';
|
||||
import { submitAudioDeviceSelectionTab, submitVideoDeviceSelectionTab } from '../../../device-selection/actions.web';
|
||||
import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection';
|
||||
import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection';
|
||||
import {
|
||||
DeviceSelection,
|
||||
getDeviceSelectionDialogProps,
|
||||
submitDeviceSelectionTab
|
||||
// @ts-ignore
|
||||
} from '../../../device-selection';
|
||||
getAudioDeviceSelectionDialogProps,
|
||||
getVideoDeviceSelectionDialogProps
|
||||
} from '../../../device-selection/functions.web';
|
||||
import {
|
||||
submitModeratorTab,
|
||||
submitMoreTab,
|
||||
|
@ -47,7 +48,6 @@ import MoreTab from './MoreTab';
|
|||
import NotificationsTab from './NotificationsTab';
|
||||
import ProfileTab from './ProfileTab';
|
||||
import ShortcutsTab from './ShortcutsTab';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -58,7 +58,7 @@ interface IProps {
|
|||
/**
|
||||
* Information about the tabs to be rendered.
|
||||
*/
|
||||
_tabs: IDialogTab[];
|
||||
_tabs: IDialogTab<any>[];
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
|
@ -100,10 +100,6 @@ const styles = (theme: Theme) => {
|
|||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
'& .calendar-tab, & .device-selection': {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .mock-atlaskit-label': {
|
||||
color: '#b8c7e0',
|
||||
fontSize: '12px',
|
||||
|
@ -168,7 +164,8 @@ const styles = (theme: Theme) => {
|
|||
flexDirection: 'column',
|
||||
fontSize: '14px',
|
||||
minHeight: '100px',
|
||||
textAlign: 'center'
|
||||
textAlign: 'center',
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .calendar-tab-sign-in': {
|
||||
|
@ -185,11 +182,6 @@ const styles = (theme: Theme) => {
|
|||
},
|
||||
|
||||
'@media only screen and (max-width: 700px)': {
|
||||
'& .device-selection': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
'& .more-tab': {
|
||||
flexDirection: 'column'
|
||||
}
|
||||
|
@ -262,15 +254,15 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
const showSoundsSettings = configuredTabs.includes('sounds');
|
||||
const enabledNotifications = getNotificationsMap(state);
|
||||
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
|
||||
const tabs: IDialogTab[] = [];
|
||||
const tabs: IDialogTab<any>[] = [];
|
||||
|
||||
if (showDeviceSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.DEVICES,
|
||||
component: DeviceSelection,
|
||||
labelKey: 'settings.devices',
|
||||
props: getDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
name: SETTINGS_TABS.AUDIO,
|
||||
component: AudioDevicesSelection,
|
||||
labelKey: 'settings.audio',
|
||||
props: getAudioDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getAudioDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
|
@ -279,14 +271,37 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
|
||||
return {
|
||||
...newProps,
|
||||
noiseSuppressionEnabled: tabState.noiseSuppressionEnabled,
|
||||
selectedAudioInputId: tabState.selectedAudioInputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} devices-pane`,
|
||||
submit: (newState: any) => submitAudioDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVolumeUp
|
||||
});
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.VIDEO,
|
||||
component: VideoDeviceSelection,
|
||||
labelKey: 'settings.video',
|
||||
props: getVideoDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getVideoDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
// tab would keep using a copy of the initial props it received,
|
||||
// leaving the device list to become stale.
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentFramerate: tabState?.currentFramerate,
|
||||
localFlipX: tabState.localFlipX,
|
||||
selectedVideoInputId: tabState.selectedVideoInputId
|
||||
};
|
||||
},
|
||||
className: `settings-pane ${classes.settingsDialog} devices-pane`,
|
||||
submit: (newState: any) => submitDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVolumeUp
|
||||
submit: (newState: any) => submitVideoDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVideo
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -314,7 +329,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
component: ModeratorTab,
|
||||
labelKey: 'settings.moderator',
|
||||
props: moderatorTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moderatorTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
|
@ -379,12 +394,11 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
component: MoreTab,
|
||||
labelKey: 'settings.more',
|
||||
props: moreTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moreTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentFramerate: tabState?.currentFramerate,
|
||||
currentLanguage: tabState?.currentLanguage,
|
||||
hideSelfView: tabState?.hideSelfView,
|
||||
showPrejoinPage: tabState?.showPrejoinPage,
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
export const SETTINGS_TABS = {
|
||||
AUDIO: 'audio_tab',
|
||||
CALENDAR: 'calendar_tab',
|
||||
DEVICES: 'devices_tab',
|
||||
MORE: 'more_tab',
|
||||
MODERATOR: 'moderator-tab',
|
||||
NOTIFICATIONS: 'notifications_tab',
|
||||
PROFILE: 'profile_tab',
|
||||
SHORTCUTS: 'shortcuts_tab'
|
||||
SHORTCUTS: 'shortcuts_tab',
|
||||
VIDEO: 'video_tab'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,8 +19,6 @@ import { getParticipantsPaneConfig } from '../participants-pane/functions';
|
|||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
|
||||
|
||||
/**
|
||||
* Used for web. Indicates if the setting section is enabled.
|
||||
*
|
||||
|
@ -118,12 +116,9 @@ export function getNotificationsMap(stateful: IStateful) {
|
|||
*/
|
||||
export function getMoreTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
|
||||
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
|
||||
|
||||
return {
|
||||
currentFramerate: framerate,
|
||||
desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
|
||||
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
|
||||
showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled,
|
||||
maxStageParticipants: state['features/base/settings'].maxStageParticipants,
|
||||
|
|
Loading…
Reference in New Issue