310 lines
9.1 KiB
TypeScript
310 lines
9.1 KiB
TypeScript
import React, { Component } from 'react';
|
|
import { WithTranslation } from 'react-i18next';
|
|
import { connect } from 'react-redux';
|
|
|
|
import { IReduxState, IStore } from '../../../../app/types';
|
|
import { openDialog } from '../../../../base/dialog/actions';
|
|
import { translate } from '../../../../base/i18n/functions';
|
|
import { IconImage } from '../../../../base/icons/svg';
|
|
import Video from '../../../../base/media/components/Video.web';
|
|
import { equals } from '../../../../base/redux/functions';
|
|
import { updateSettings } from '../../../../base/settings/actions';
|
|
import Checkbox from '../../../../base/ui/components/web/Checkbox';
|
|
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
|
|
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
|
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
|
|
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
|
|
import { createLocalVideoTracks } from '../../../functions.web';
|
|
|
|
const videoClassName = 'video-preview-video flipVideoX';
|
|
|
|
/**
|
|
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
|
|
*/
|
|
export interface IProps extends WithTranslation {
|
|
|
|
/**
|
|
* Callback to change the flip state.
|
|
*/
|
|
changeFlip: (flip: boolean) => void;
|
|
|
|
/**
|
|
* The deviceId of the camera device currently being used.
|
|
*/
|
|
currentCameraDeviceId: string;
|
|
|
|
/**
|
|
* Whether or not the local video is flipped.
|
|
*/
|
|
localFlipX: boolean;
|
|
|
|
/**
|
|
* Open virtual background dialog.
|
|
*/
|
|
selectBackground: () => void;
|
|
|
|
/**
|
|
* Callback invoked to change current camera.
|
|
*/
|
|
setVideoInputDevice: Function;
|
|
|
|
/**
|
|
* Callback invoked to toggle the settings popup visibility.
|
|
*/
|
|
toggleVideoSettings: Function;
|
|
|
|
/**
|
|
* All the camera device ids currently connected.
|
|
*/
|
|
videoDeviceIds: string[];
|
|
}
|
|
|
|
/**
|
|
* The type of the React {@code Component} state of {@link VideoSettingsContent}.
|
|
*/
|
|
type State = {
|
|
|
|
/**
|
|
* An array of all the jitsiTracks and eventual errors.
|
|
*/
|
|
trackData: { deviceId: string; error?: string; jitsiTrack: any | null; }[];
|
|
};
|
|
|
|
/**
|
|
* Implements a React {@link Component} which displays a list of video
|
|
* previews to choose from.
|
|
*
|
|
* @augments Component
|
|
*/
|
|
class VideoSettingsContent extends Component<IProps, State> {
|
|
_componentWasUnmounted: boolean;
|
|
|
|
/**
|
|
* Initializes a new {@code VideoSettingsContent} instance.
|
|
*
|
|
* @param {Object} props - The read-only properties with which the new
|
|
* instance is to be initialized.
|
|
*/
|
|
constructor(props: IProps) {
|
|
super(props);
|
|
this._onToggleFlip = this._onToggleFlip.bind(this);
|
|
|
|
this.state = {
|
|
trackData: new Array(props.videoDeviceIds.length).fill({
|
|
jitsiTrack: null
|
|
})
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggles local video flip state.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onToggleFlip() {
|
|
const { localFlipX, changeFlip } = this.props;
|
|
|
|
changeFlip(!localFlipX);
|
|
}
|
|
|
|
/**
|
|
* Creates and updates the track data.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
async _setTracks() {
|
|
this._disposeTracks(this.state.trackData);
|
|
|
|
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
|
|
if (this._componentWasUnmounted) {
|
|
this._disposeTracks(trackData);
|
|
} else {
|
|
this.setState({
|
|
trackData
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys all the tracks from trackData object.
|
|
*
|
|
* @param {Object[]} trackData - An array of tracks that are to be disposed.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
_disposeTracks(trackData: { jitsiTrack: any; }[]) {
|
|
trackData.forEach(({ jitsiTrack }) => {
|
|
jitsiTrack?.dispose();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the click handler used when selecting the video preview.
|
|
*
|
|
* @param {string} deviceId - The id of the camera device.
|
|
* @returns {Function}
|
|
*/
|
|
_onEntryClick(deviceId: string) {
|
|
return () => {
|
|
this.props.setVideoInputDevice(deviceId);
|
|
this.props.toggleVideoSettings();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Renders a preview entry.
|
|
*
|
|
* @param {Object} data - The track data.
|
|
* @param {number} index - The index of the entry.
|
|
* @returns {React$Node}
|
|
*/
|
|
_renderPreviewEntry(data: { deviceId: string; error?: string; jitsiTrack: any | null; }, index: number) {
|
|
const { error, jitsiTrack, deviceId } = data;
|
|
const { currentCameraDeviceId, t } = this.props;
|
|
const isSelected = deviceId === currentCameraDeviceId;
|
|
const key = `vp-${index}`;
|
|
const className = 'video-preview-entry';
|
|
const tabIndex = '0';
|
|
|
|
if (error) {
|
|
return (
|
|
<div
|
|
className = { className }
|
|
key = { key }
|
|
tabIndex = { -1 } >
|
|
<div className = 'video-preview-error'>{t(error)}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const props: any = {
|
|
className,
|
|
key,
|
|
tabIndex
|
|
};
|
|
const label = jitsiTrack?.getTrackLabel();
|
|
|
|
if (isSelected) {
|
|
props['aria-checked'] = true;
|
|
props.className = `${className} video-preview-entry--selected`;
|
|
} else {
|
|
props.onClick = this._onEntryClick(deviceId);
|
|
props.onKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
props.onClick();
|
|
}
|
|
};
|
|
}
|
|
|
|
return (
|
|
<div
|
|
{ ...props }
|
|
role = 'radio'>
|
|
<div className = 'video-preview-label'>
|
|
{label && <div className = 'video-preview-label-text'>
|
|
<span>{label}</span>
|
|
</div>}
|
|
</div>
|
|
<Video
|
|
className = { videoClassName }
|
|
playsinline = { true }
|
|
videoTrack = {{ jitsiTrack }} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentDidMount}.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
componentDidMount() {
|
|
this._setTracks();
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentWillUnmount}.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
componentWillUnmount() {
|
|
this._componentWasUnmounted = true;
|
|
this._disposeTracks(this.state.trackData);
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentDidUpdate}.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
componentDidUpdate(prevProps: IProps) {
|
|
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
|
|
this._setTracks();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#render}.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
render() {
|
|
const { trackData } = this.state;
|
|
const { selectBackground, t, localFlipX } = this.props;
|
|
|
|
return (
|
|
<ContextMenu
|
|
aria-labelledby = 'video-settings-button'
|
|
className = 'video-preview-container'
|
|
hidden = { false }
|
|
id = 'video-settings-dialog'
|
|
role = 'radiogroup'
|
|
tabIndex = { -1 }>
|
|
<ContextMenuItemGroup>
|
|
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
|
</ContextMenuItemGroup>
|
|
<ContextMenuItemGroup>
|
|
<ContextMenuItem
|
|
accessibilityLabel = 'virtualBackground.title'
|
|
icon = { IconImage }
|
|
onClick = { selectBackground }
|
|
text = { t('virtualBackground.title') } />
|
|
<div
|
|
className = 'video-preview-checkbox-container'
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick = { e => e.stopPropagation() }>
|
|
<Checkbox
|
|
checked = { localFlipX }
|
|
label = { t('videothumbnail.mirrorVideo') }
|
|
onChange = { this._onToggleFlip } />
|
|
</div>
|
|
</ContextMenuItemGroup>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
}
|
|
|
|
const mapStateToProps = (state: IReduxState) => {
|
|
const { localFlipX } = state['features/base/settings'];
|
|
|
|
return {
|
|
localFlipX: Boolean(localFlipX)
|
|
};
|
|
};
|
|
|
|
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
|
|
return {
|
|
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
|
|
changeFlip: (flip: boolean) => {
|
|
dispatch(updateSettings({
|
|
localFlipX: flip
|
|
}));
|
|
}
|
|
};
|
|
};
|
|
|
|
export default translate(connect(mapStateToProps, mapDispatchToProps)(VideoSettingsContent));
|