This commit is contained in:
23
react/features/virtual-background/actionTypes.ts
Normal file
23
react/features/virtual-background/actionTypes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* The type of redux action dispatched which represents that the background
|
||||
* effect is enabled or not.
|
||||
*
|
||||
* @returns {{
|
||||
* type: BACKGROUND_ENABLED,
|
||||
* backgroundEffectEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export const BACKGROUND_ENABLED = 'BACKGROUND_ENABLED';
|
||||
|
||||
/**
|
||||
* The type of the action which enables or disables virtual background
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_VIRTUAL_BACKGROUND,
|
||||
* virtualSource: string,
|
||||
* blurValue: number,
|
||||
* backgroundType: string,
|
||||
* selectedThumbnail: string
|
||||
* }}
|
||||
*/
|
||||
export const SET_VIRTUAL_BACKGROUND = 'SET_VIRTUAL_BACKGROUND';
|
||||
105
react/features/virtual-background/actions.ts
Normal file
105
react/features/virtual-background/actions.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { createVirtualBackgroundEffect } from '../stream-effects/virtual-background';
|
||||
|
||||
import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes';
|
||||
import { VIRTUAL_BACKGROUND_TYPE } from './constants';
|
||||
import logger from './logger';
|
||||
import { IVirtualBackground } from './reducer';
|
||||
|
||||
/**
|
||||
* Signals the local participant activate the virtual background video or not.
|
||||
*
|
||||
* @param {Object} options - Represents the virtual background set options.
|
||||
* @param {Object} jitsiTrack - Represents the jitsi track that will have backgraund effect applied.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function toggleBackgroundEffect(options: IVirtualBackground, jitsiTrack: any) {
|
||||
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
dispatch(backgroundEnabled(options.backgroundEffectEnabled));
|
||||
dispatch(setVirtualBackground(options));
|
||||
const state = getState();
|
||||
const virtualBackground = state['features/virtual-background'];
|
||||
|
||||
if (jitsiTrack) {
|
||||
try {
|
||||
if (options.backgroundEffectEnabled) {
|
||||
await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground, dispatch));
|
||||
} else {
|
||||
await jitsiTrack.setEffect(undefined);
|
||||
dispatch(backgroundEnabled(false));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(backgroundEnabled(false));
|
||||
logger.error('Error on apply background effect:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected virtual background image object.
|
||||
*
|
||||
* @param {Object} options - Represents the virtual background set options.
|
||||
* @returns {{
|
||||
* type: SET_VIRTUAL_BACKGROUND,
|
||||
* virtualSource: string,
|
||||
* blurValue: number,
|
||||
* type: string,
|
||||
* }}
|
||||
*/
|
||||
export function setVirtualBackground(options?: IVirtualBackground) {
|
||||
return {
|
||||
type: SET_VIRTUAL_BACKGROUND,
|
||||
virtualSource: options?.virtualSource,
|
||||
blurValue: options?.blurValue,
|
||||
backgroundType: options?.backgroundType,
|
||||
selectedThumbnail: options?.selectedThumbnail
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the local participant that the background effect has been enabled.
|
||||
*
|
||||
* @param {boolean} backgroundEffectEnabled - Indicate if virtual background effect is activated.
|
||||
* @returns {{
|
||||
* type: BACKGROUND_ENABLED,
|
||||
* backgroundEffectEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function backgroundEnabled(backgroundEffectEnabled?: boolean) {
|
||||
return {
|
||||
type: BACKGROUND_ENABLED,
|
||||
backgroundEffectEnabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates blurred background selection/removal on video background. Used by API only.
|
||||
*
|
||||
* @param {JitsiLocalTrack} videoTrack - The targeted video track.
|
||||
* @param {string} [blurType] - Blur type to apply. Accepted values are 'slight-blur', 'blur' or 'none'.
|
||||
* @param {boolean} muted - Muted state of the video track.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function toggleBlurredBackgroundEffect(videoTrack: any, blurType: 'slight-blur' | 'blur' | 'none',
|
||||
muted: boolean) {
|
||||
return async function(dispatch: IStore['dispatch'], _getState: IStore['getState']) {
|
||||
if (muted || !videoTrack || !blurType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blurType === 'none') {
|
||||
dispatch(toggleBackgroundEffect({
|
||||
backgroundEffectEnabled: false,
|
||||
selectedThumbnail: blurType
|
||||
}, videoTrack));
|
||||
} else {
|
||||
dispatch(toggleBackgroundEffect({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
|
||||
blurValue: blurType === 'blur' ? 25 : 8,
|
||||
selectedThumbnail: blurType
|
||||
}, videoTrack));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconPlus } from '../../base/icons/svg';
|
||||
import { type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
|
||||
import { resizeImage } from '../functions';
|
||||
import logger from '../logger';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Callback used to set the 'loading' state of the parent component.
|
||||
*/
|
||||
setLoading: Function;
|
||||
|
||||
/**
|
||||
* Callback used to set the options.
|
||||
*/
|
||||
setOptions: Function;
|
||||
|
||||
/**
|
||||
* Callback used to set the storedImages array.
|
||||
*/
|
||||
setStoredImages: Function;
|
||||
|
||||
/**
|
||||
* If a label should be displayed alongside the button.
|
||||
*/
|
||||
showLabel: boolean;
|
||||
|
||||
/**
|
||||
* A list of images locally stored.
|
||||
*/
|
||||
storedImages: Array<Image>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
label: {
|
||||
...theme.typography.bodyShortBold,
|
||||
color: theme.palette.link01,
|
||||
marginBottom: theme.spacing(3),
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
||||
addBackground: {
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'& svg': {
|
||||
fill: `${theme.palette.link01} !important`
|
||||
}
|
||||
},
|
||||
|
||||
input: {
|
||||
display: 'none'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to upload an image.
|
||||
*
|
||||
* @param {Object} Props - The props of the component.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
function UploadImageButton({
|
||||
setLoading,
|
||||
setOptions,
|
||||
setStoredImages,
|
||||
showLabel,
|
||||
storedImages,
|
||||
t
|
||||
}: IProps) {
|
||||
const { classes } = useStyles();
|
||||
const uploadImageButton = useRef<HTMLInputElement>(null);
|
||||
const uploadImageKeyPress = useCallback(e => {
|
||||
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
uploadImageButton.current.click();
|
||||
}
|
||||
}, [ uploadImageButton.current ]);
|
||||
|
||||
|
||||
const uploadImage = useCallback(async e => {
|
||||
const imageFile = e.target.files;
|
||||
|
||||
if (imageFile.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.readAsDataURL(imageFile[0]);
|
||||
reader.onload = async () => {
|
||||
const url = await resizeImage(reader.result);
|
||||
const uuId = uuidv4();
|
||||
|
||||
setStoredImages([
|
||||
...storedImages,
|
||||
{
|
||||
id: uuId,
|
||||
src: url
|
||||
}
|
||||
]);
|
||||
setOptions({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
|
||||
selectedThumbnail: uuId,
|
||||
virtualSource: url
|
||||
});
|
||||
};
|
||||
logger.info('New virtual background image uploaded!');
|
||||
|
||||
reader.onerror = () => {
|
||||
setLoading(false);
|
||||
logger.error('Failed to upload virtual image!');
|
||||
};
|
||||
}, [ storedImages ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLabel && <label
|
||||
className = { classes.label }
|
||||
htmlFor = 'file-upload'
|
||||
onKeyPress = { uploadImageKeyPress }
|
||||
tabIndex = { 0 } >
|
||||
<Icon
|
||||
className = { classes.addBackground }
|
||||
size = { 24 }
|
||||
src = { IconPlus } />
|
||||
{t('virtualBackground.addBackground')}
|
||||
</label>}
|
||||
|
||||
<input
|
||||
accept = 'image/*'
|
||||
className = { classes.input }
|
||||
id = 'file-upload'
|
||||
onChange = { uploadImage }
|
||||
ref = { uploadImageButton }
|
||||
role = 'button'
|
||||
type = 'file' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(UploadImageButton);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconImage } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { isScreenVideoShared } from '../../screen-share/functions';
|
||||
import { openSettingsDialog } from '../../settings/actions';
|
||||
import { SETTINGS_TABS } from '../../settings/constants';
|
||||
import { checkBlurSupport, checkVirtualBackgroundEnabled } from '../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoBackgroundButton}.
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* True if the video background is blurred or false if it is not.
|
||||
*/
|
||||
_isBackgroundEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract implementation of a button that toggles the video background dialog.
|
||||
*/
|
||||
class VideoBackgroundButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.selectBackground';
|
||||
override icon = IconImage;
|
||||
override label = 'toolbar.selectBackground';
|
||||
override tooltip = 'toolbar.selectBackground';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and toggles the virtual background dialog
|
||||
* state accordingly.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code boolean} value indicating if the background effect is
|
||||
* enabled or not.
|
||||
*
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._isBackgroundEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code VideoBackgroundButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isBackgroundEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
|
||||
return {
|
||||
_isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled),
|
||||
visible: checkBlurSupport()
|
||||
&& !isScreenVideoShared(state)
|
||||
&& checkVirtualBackgroundEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(VideoBackgroundButton));
|
||||
@@ -0,0 +1,305 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { hideDialog } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { Video } from '../../base/media/components/index';
|
||||
import { equals } from '../../base/redux/functions';
|
||||
import { createLocalTracksF } from '../../base/tracks/functions';
|
||||
import Spinner from '../../base/ui/components/web/Spinner';
|
||||
import { showWarningNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { toggleBackgroundEffect } from '../actions';
|
||||
import logger from '../logger';
|
||||
import { IVirtualBackground } from '../reducer';
|
||||
|
||||
/**
|
||||
* The type of the React {@code PureComponent} props of {@link VirtualBackgroundPreview}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Dialog callback that indicates if the background preview was loaded.
|
||||
*/
|
||||
loadedPreview: Function;
|
||||
|
||||
/**
|
||||
* Represents the virtual background set options.
|
||||
*/
|
||||
options: IVirtualBackground;
|
||||
|
||||
/**
|
||||
* The id of the selected video device.
|
||||
*/
|
||||
selectedVideoInputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VirtualBackgroundPreview}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Activate the selected device camera only.
|
||||
*/
|
||||
jitsiTrack: Object | null;
|
||||
|
||||
/**
|
||||
* Loader activated on setting virtual background.
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* Flag that indicates if the local track was loaded.
|
||||
*/
|
||||
localTrackLoaded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
virtualBackgroundPreview: {
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
marginBottom: theme.spacing(3),
|
||||
zIndex: 2,
|
||||
borderRadius: '3px',
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
position: 'relative' as const
|
||||
},
|
||||
|
||||
previewLoader: {
|
||||
height: '220px',
|
||||
|
||||
'& svg': {
|
||||
position: 'absolute' as const,
|
||||
top: '40%',
|
||||
left: '45%'
|
||||
}
|
||||
},
|
||||
|
||||
previewVideo: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
objectFit: 'cover' as const
|
||||
},
|
||||
|
||||
error: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '220px',
|
||||
position: 'relative' as const
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link PureComponent} which displays the virtual
|
||||
* background preview.
|
||||
*
|
||||
* @augments PureComponent
|
||||
*/
|
||||
class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
|
||||
_componentWasUnmounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VirtualBackgroundPreview} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
localTrackLoaded: false,
|
||||
jitsiTrack: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the jitsiTrack object.
|
||||
*
|
||||
* @param {Object} jitsiTrack - The track that needs to be disposed.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_stopStream(jitsiTrack: any) {
|
||||
if (jitsiTrack) {
|
||||
jitsiTrack.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and updates the track data.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async _setTracks() {
|
||||
try {
|
||||
this.setState({ loading: true });
|
||||
const [ jitsiTrack ] = await createLocalTracksF({
|
||||
cameraDeviceId: this.props.selectedVideoInputId,
|
||||
devices: [ 'video' ]
|
||||
});
|
||||
|
||||
this.setState({ localTrackLoaded: true });
|
||||
|
||||
// In case the component gets unmounted before the tracks are created
|
||||
// avoid a leak by not setting the state
|
||||
if (this._componentWasUnmounted) {
|
||||
this._stopStream(jitsiTrack);
|
||||
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
jitsiTrack,
|
||||
loading: false
|
||||
});
|
||||
this.props.loadedPreview(true);
|
||||
} catch (error) {
|
||||
this.props.dispatch(hideDialog());
|
||||
this.props.dispatch(
|
||||
showWarningNotification({
|
||||
titleKey: 'virtualBackground.backgroundEffectError',
|
||||
descriptionKey: 'deviceError.cameraError'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG)
|
||||
);
|
||||
logger.error('Failed to access camera device. Error on apply background effect.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply background effect on video preview.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async _applyBackgroundEffect() {
|
||||
this.setState({ loading: true });
|
||||
this.props.loadedPreview(false);
|
||||
await this.props.dispatch(toggleBackgroundEffect(this.props.options, this.state.jitsiTrack));
|
||||
this.props.loadedPreview(true);
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply video preview loader.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_loadVideoPreview() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div className = { classes.previewLoader }>
|
||||
<Spinner size = 'large' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a preview entry.
|
||||
*
|
||||
* @param {Object} data - The track data.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderPreviewEntry(data: Object) {
|
||||
const { t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
if (this.state.loading) {
|
||||
return this._loadVideoPreview();
|
||||
}
|
||||
if (!data) {
|
||||
return (
|
||||
<div className = { classes.error }>{t('deviceSelection.previewUnavailable')}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Video
|
||||
className = { classes.previewVideo }
|
||||
id = 'virtual_background_preview'
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: data }} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this._setTracks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this._componentWasUnmounted = true;
|
||||
this._stopStream(this.state.jitsiTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override async componentDidUpdate(prevProps: IProps) {
|
||||
if (!equals(this.props.selectedVideoInputId, prevProps.selectedVideoInputId)) {
|
||||
this._setTracks();
|
||||
}
|
||||
if (!equals(this.props.options, prevProps.options) && this.state.localTrackLoaded) {
|
||||
this._applyBackgroundEffect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { jitsiTrack } = this.state;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div className = { classes.virtualBackgroundPreview }>
|
||||
{jitsiTrack
|
||||
? this._renderPreviewEntry(jitsiTrack)
|
||||
: this._loadVideoPreview()
|
||||
}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(withStyles(VirtualBackgroundPreview, styles)));
|
||||
@@ -0,0 +1,503 @@
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { safeJsonParse } from '@jitsi/js-utils/json';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../base/icons/svg';
|
||||
import Tooltip from '../../base/tooltip/components/Tooltip';
|
||||
import Spinner from '../../base/ui/components/web/Spinner';
|
||||
import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
|
||||
import { toDataURL } from '../functions';
|
||||
import logger from '../logger';
|
||||
import { IVirtualBackground } from '../reducer';
|
||||
|
||||
import UploadImageButton from './UploadImageButton';
|
||||
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The list of Images to choose from.
|
||||
*/
|
||||
_images: Array<Image>;
|
||||
|
||||
/**
|
||||
* If the upload button should be displayed or not.
|
||||
*/
|
||||
_showUploadButton: boolean;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Options change handler.
|
||||
*/
|
||||
onOptionsChange: Function;
|
||||
|
||||
/**
|
||||
* Virtual background options.
|
||||
*/
|
||||
options: IVirtualBackground;
|
||||
|
||||
/**
|
||||
* Returns the selected thumbnail identifier.
|
||||
*/
|
||||
selectedThumbnail: string;
|
||||
|
||||
/**
|
||||
* The id of the selected video device.
|
||||
*/
|
||||
selectedVideoInputId: string;
|
||||
}
|
||||
|
||||
const onError = (event: any) => {
|
||||
event.target.style.display = 'none';
|
||||
};
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
virtualBackgroundLoading: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '50px'
|
||||
},
|
||||
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
thumbnailContainer: {
|
||||
width: '100%',
|
||||
display: 'inline-grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
|
||||
gap: theme.spacing(1),
|
||||
|
||||
'@media (min-width: 608px) and (max-width: 712px)': {
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
},
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gap: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
|
||||
thumbnail: {
|
||||
height: '54px',
|
||||
width: '100%',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
...theme.typography.labelBold,
|
||||
color: theme.palette.text01,
|
||||
objectFit: 'cover',
|
||||
|
||||
[[ '&:hover', '&:focus' ] as any]: {
|
||||
opacity: 0.5,
|
||||
cursor: 'pointer',
|
||||
|
||||
'& ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
height: '70px'
|
||||
}
|
||||
},
|
||||
|
||||
selectedThumbnail: {
|
||||
border: `2px solid ${theme.palette.action01Hover}`
|
||||
},
|
||||
|
||||
noneThumbnail: {
|
||||
backgroundColor: theme.palette.ui04
|
||||
},
|
||||
|
||||
slightBlur: {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#a4a4a4'
|
||||
},
|
||||
|
||||
blur: {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#7e8287'
|
||||
},
|
||||
|
||||
storedImageContainer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'&:focus-within .delete-image-container': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
|
||||
deleteImageIcon: {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
right: '3px',
|
||||
background: theme.palette.ui03,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
display: 'block',
|
||||
padding: '3px'
|
||||
},
|
||||
|
||||
[[ '&:hover', '&:focus' ] as any]: {
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders virtual background dialog.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function VirtualBackgrounds({
|
||||
_images,
|
||||
_showUploadButton,
|
||||
onOptionsChange,
|
||||
options,
|
||||
selectedVideoInputId,
|
||||
t
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
|
||||
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
|
||||
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && safeJsonParse(localImages)) || []);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
|
||||
const deleteStoredImage = useCallback(e => {
|
||||
const imageId = e.currentTarget.getAttribute('data-imageid');
|
||||
|
||||
setStoredImages(storedImages.filter(item => item.id !== imageId));
|
||||
}, [ storedImages ]);
|
||||
|
||||
const deleteStoredImageKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
deleteStoredImage(e);
|
||||
}
|
||||
}, [ deleteStoredImage ]);
|
||||
|
||||
/**
|
||||
* Updates stored images on local storage.
|
||||
*/
|
||||
useEffect(() => {
|
||||
try {
|
||||
jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
|
||||
} catch (err) {
|
||||
// Preventing localStorage QUOTA_EXCEEDED_ERR
|
||||
err && setStoredImages(storedImages.slice(1));
|
||||
}
|
||||
if (storedImages.length === BACKGROUNDS_LIMIT) {
|
||||
setStoredImages(storedImages.slice(1));
|
||||
}
|
||||
}, [ storedImages ]);
|
||||
|
||||
const enableBlur = useCallback(async () => {
|
||||
onOptionsChange({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
|
||||
blurValue: 25,
|
||||
selectedThumbnail: 'blur'
|
||||
});
|
||||
logger.info('"Blur" option set for virtual background preview!');
|
||||
|
||||
}, []);
|
||||
|
||||
const enableBlurKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
enableBlur();
|
||||
}
|
||||
}, [ enableBlur ]);
|
||||
|
||||
const enableSlideBlur = useCallback(async () => {
|
||||
onOptionsChange({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
|
||||
blurValue: 8,
|
||||
selectedThumbnail: 'slight-blur'
|
||||
});
|
||||
logger.info('"Slight-blur" option set for virtual background preview!');
|
||||
|
||||
}, []);
|
||||
|
||||
const enableSlideBlurKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
enableSlideBlur();
|
||||
}
|
||||
}, [ enableSlideBlur ]);
|
||||
|
||||
const removeBackground = useCallback(async () => {
|
||||
onOptionsChange({
|
||||
backgroundEffectEnabled: false,
|
||||
selectedThumbnail: 'none'
|
||||
});
|
||||
logger.info('"None" option set for virtual background preview!');
|
||||
|
||||
}, []);
|
||||
|
||||
const removeBackgroundKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
removeBackground();
|
||||
}
|
||||
}, [ removeBackground ]);
|
||||
|
||||
const setUploadedImageBackground = useCallback(async e => {
|
||||
const imageId = e.currentTarget.getAttribute('data-imageid');
|
||||
const image = storedImages.find(img => img.id === imageId);
|
||||
|
||||
if (image) {
|
||||
onOptionsChange({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
|
||||
selectedThumbnail: image.id,
|
||||
virtualSource: image.src
|
||||
});
|
||||
logger.info('Uploaded image set for virtual background preview!');
|
||||
}
|
||||
}, [ storedImages ]);
|
||||
|
||||
const setImageBackground = useCallback(async e => {
|
||||
const imageId = e.currentTarget.getAttribute('data-imageid');
|
||||
const image = _images.find(img => img.id === imageId);
|
||||
|
||||
if (image) {
|
||||
try {
|
||||
const url = await toDataURL(image.src);
|
||||
|
||||
onOptionsChange({
|
||||
backgroundEffectEnabled: true,
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
|
||||
selectedThumbnail: image.id,
|
||||
virtualSource: url
|
||||
});
|
||||
logger.info('Image set for virtual background preview!');
|
||||
} catch (err) {
|
||||
logger.error('Could not fetch virtual background image:', err);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setImageBackgroundKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setImageBackground(e);
|
||||
}
|
||||
}, [ setImageBackground ]);
|
||||
|
||||
const setUploadedImageBackgroundKeyPress = useCallback(e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setUploadedImageBackground(e);
|
||||
}
|
||||
}, [ setUploadedImageBackground ]);
|
||||
|
||||
const loadedPreviewState = useCallback(async loaded => {
|
||||
await setPreviewIsLoaded(loaded);
|
||||
}, []);
|
||||
|
||||
// create a full list of {backgroundId: backgroundLabel} to easily retrieve label of selected background
|
||||
const labelsMap: Record<string, string> = {
|
||||
none: t('virtualBackground.none'),
|
||||
'slight-blur': t('virtualBackground.slightBlur'),
|
||||
blur: t('virtualBackground.blur'),
|
||||
..._images.reduce<Record<string, string>>((acc, image) => {
|
||||
acc[image.id] = image.tooltip ? t(`virtualBackground.${image.tooltip}`) : '';
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
...storedImages.reduce<Record<string, string>>((acc, image, index) => {
|
||||
acc[image.id] = t('virtualBackground.uploadedImage', { index: index + 1 });
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
const currentBackgroundLabel = options?.selectedThumbnail ? labelsMap[options.selectedThumbnail] : labelsMap.none;
|
||||
const isThumbnailSelected = useCallback(thumbnail => options?.selectedThumbnail === thumbnail, [ options ]);
|
||||
const getSelectedThumbnailClass = useCallback(
|
||||
thumbnail => isThumbnailSelected(thumbnail) && classes.selectedThumbnail, [ isThumbnailSelected, options ]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualBackgroundPreview
|
||||
loadedPreview = { loadedPreviewState }
|
||||
options = { options }
|
||||
selectedVideoInputId = { selectedVideoInputId } />
|
||||
{loading ? (
|
||||
<div className = { classes.virtualBackgroundLoading }>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className = { classes.container }>
|
||||
<span
|
||||
className = 'sr-only'
|
||||
id = 'virtual-background-current-info'>
|
||||
{ t('virtualBackground.accessibilityLabel.currentBackground', {
|
||||
background: currentBackgroundLabel
|
||||
}) }
|
||||
</span>
|
||||
{_showUploadButton
|
||||
&& <UploadImageButton
|
||||
setLoading = { setLoading }
|
||||
setOptions = { onOptionsChange }
|
||||
setStoredImages = { setStoredImages }
|
||||
showLabel = { previewIsLoaded }
|
||||
storedImages = { storedImages } />}
|
||||
<div
|
||||
aria-describedby = 'virtual-background-current-info'
|
||||
aria-label = { t('virtualBackground.accessibilityLabel.selectBackground') }
|
||||
className = { classes.thumbnailContainer }
|
||||
role = 'radiogroup'
|
||||
tabIndex = { -1 }>
|
||||
<Tooltip
|
||||
content = { t('virtualBackground.removeBackground') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { isThumbnailSelected('none') }
|
||||
aria-label = { t('virtualBackground.removeBackground') }
|
||||
className = { cx(classes.thumbnail, classes.noneThumbnail,
|
||||
getSelectedThumbnailClass('none')) }
|
||||
onClick = { removeBackground }
|
||||
onKeyPress = { removeBackgroundKeyPress }
|
||||
role = 'radio'
|
||||
tabIndex = { 0 } >
|
||||
{t('virtualBackground.none')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content = { t('virtualBackground.slightBlur') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { isThumbnailSelected('slight-blur') }
|
||||
aria-label = { t('virtualBackground.slightBlur') }
|
||||
className = { cx(classes.thumbnail, classes.slightBlur,
|
||||
getSelectedThumbnailClass('slight-blur')) }
|
||||
onClick = { enableSlideBlur }
|
||||
onKeyPress = { enableSlideBlurKeyPress }
|
||||
role = 'radio'
|
||||
tabIndex = { 0 }>
|
||||
{t('virtualBackground.slightBlur')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content = { t('virtualBackground.blur') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { isThumbnailSelected('blur') }
|
||||
aria-label = { t('virtualBackground.blur') }
|
||||
className = { cx(classes.thumbnail, classes.blur,
|
||||
getSelectedThumbnailClass('blur')) }
|
||||
onClick = { enableBlur }
|
||||
onKeyPress = { enableBlurKeyPress }
|
||||
role = 'radio'
|
||||
tabIndex = { 0 }>
|
||||
{t('virtualBackground.blur')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{_images.map(image => (
|
||||
<Tooltip
|
||||
content = { (image.tooltip && t(`virtualBackground.${image.tooltip}`)) ?? '' }
|
||||
key = { image.id }
|
||||
position = { 'top' }>
|
||||
<img
|
||||
alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
|
||||
aria-checked = { isThumbnailSelected(image.id) }
|
||||
className = { cx(classes.thumbnail,
|
||||
getSelectedThumbnailClass(image.id)) }
|
||||
data-imageid = { image.id }
|
||||
onClick = { setImageBackground }
|
||||
onError = { onError }
|
||||
onKeyPress = { setImageBackgroundKeyPress }
|
||||
role = 'radio'
|
||||
src = { image.src }
|
||||
tabIndex = { 0 } />
|
||||
</Tooltip>
|
||||
))}
|
||||
{storedImages.map((image, index) => (
|
||||
<div
|
||||
className = { classes.storedImageContainer }
|
||||
key = { image.id }>
|
||||
<img
|
||||
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
|
||||
aria-checked = { isThumbnailSelected(image.id) }
|
||||
className = { cx(classes.thumbnail,
|
||||
getSelectedThumbnailClass(image.id)) }
|
||||
data-imageid = { image.id }
|
||||
onClick = { setUploadedImageBackground }
|
||||
onError = { onError }
|
||||
onKeyPress = { setUploadedImageBackgroundKeyPress }
|
||||
role = 'radio'
|
||||
src = { image.src }
|
||||
tabIndex = { 0 } />
|
||||
|
||||
<Icon
|
||||
ariaLabel = { t('virtualBackground.deleteImage') }
|
||||
className = { cx(classes.deleteImageIcon, 'delete-image-icon') }
|
||||
data-imageid = { image.id }
|
||||
onClick = { deleteStoredImage }
|
||||
onKeyPress = { deleteStoredImageKeyPress }
|
||||
role = 'button'
|
||||
size = { 16 }
|
||||
src = { IconCloseLarge }
|
||||
tabIndex = { 0 } />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code VirtualBackground} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{Props}}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
|
||||
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
|
||||
|
||||
return {
|
||||
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
|
||||
_showUploadButton: !state['features/base/config'].disableAddingBackgroundImages
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(translate(VirtualBackgrounds));
|
||||
61
react/features/virtual-background/constants.ts
Normal file
61
react/features/virtual-background/constants.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* An enumeration of the different virtual background types.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const VIRTUAL_BACKGROUND_TYPE = {
|
||||
IMAGE: 'image',
|
||||
BLUR: 'blur',
|
||||
NONE: 'none'
|
||||
};
|
||||
|
||||
|
||||
export type Image = {
|
||||
id: string;
|
||||
src: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
// The limit of virtual background uploads is 24. When the number
|
||||
// of uploads is 25 we trigger the deleteStoredImage function to delete
|
||||
// the first/oldest uploaded background.
|
||||
export const BACKGROUNDS_LIMIT = 25;
|
||||
|
||||
|
||||
export const IMAGES: Array<Image> = [
|
||||
{
|
||||
tooltip: 'image1',
|
||||
id: '1',
|
||||
src: 'images/virtual-background/background-1.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image2',
|
||||
id: '2',
|
||||
src: 'images/virtual-background/background-2.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image3',
|
||||
id: '3',
|
||||
src: 'images/virtual-background/background-3.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image4',
|
||||
id: '4',
|
||||
src: 'images/virtual-background/background-4.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image5',
|
||||
id: '5',
|
||||
src: 'images/virtual-background/background-5.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image6',
|
||||
id: '6',
|
||||
src: 'images/virtual-background/background-6.jpg'
|
||||
},
|
||||
{
|
||||
tooltip: 'image7',
|
||||
id: '7',
|
||||
src: 'images/virtual-background/background-7.jpg'
|
||||
}
|
||||
];
|
||||
119
react/features/virtual-background/functions.ts
Normal file
119
react/features/virtual-background/functions.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
|
||||
let filterSupport: boolean | undefined;
|
||||
|
||||
/**
|
||||
* Checks context filter support.
|
||||
*
|
||||
* @returns {boolean} True if the filter is supported and false if the filter is not supported by the browser.
|
||||
*/
|
||||
export function checkBlurSupport() {
|
||||
if (typeof filterSupport === 'undefined') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
filterSupport = typeof ctx?.filter !== 'undefined';
|
||||
|
||||
canvas.remove();
|
||||
}
|
||||
|
||||
return filterSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if virtual background is enabled.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the app.
|
||||
* @returns {boolean} True if virtual background is enabled and false if virtual background is disabled.
|
||||
*/
|
||||
export function checkVirtualBackgroundEnabled(state: IReduxState) {
|
||||
return state['features/base/config'].disableVirtualBackground !== true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert blob to base64.
|
||||
*
|
||||
* @param {Blob} blob - The link to add info with.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const blobToData = (blob: Blob) =>
|
||||
new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => resolve(reader.result?.toString());
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert blob to base64.
|
||||
*
|
||||
* @param {string} url - The image url.
|
||||
* @returns {Object} - Returns the converted blob to base64.
|
||||
*/
|
||||
export const toDataURL = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const resData = await blobToData(blob);
|
||||
|
||||
return resData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resize image and adjust original aspect ratio.
|
||||
*
|
||||
* @param {Object} base64image - Base64 image extraction.
|
||||
* @param {number} width - Value for resizing the image width.
|
||||
* @param {number} height - Value for resizing the image height.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export function resizeImage(base64image: any, width = 1920, height = 1080): Promise<string> {
|
||||
|
||||
// In order to work on Firefox browser we need to handle the asynchronous nature of image loading; We need to use
|
||||
// a promise mechanism. The reason why it 'works' without this mechanism in Chrome is actually 'by accident' because
|
||||
// the image happens to be in the cache and the browser is able to deliver the uncompressed/decoded image
|
||||
// before using the image in the drawImage call.
|
||||
return new Promise(resolve => {
|
||||
const img = document.createElement('img');
|
||||
|
||||
img.onload = function() {
|
||||
// Create an off-screen canvas.
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
// Set its dimension to target size.
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Draw source image into the off-screen canvas.
|
||||
// TODO: keep aspect ratio and implement object-fit: cover.
|
||||
context?.drawImage(img as any, 0, 0, width, height);
|
||||
|
||||
// Encode image to data-uri with base64 version of compressed image.
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.5));
|
||||
};
|
||||
img.src = base64image;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating a wrapper for promises on a specific time interval.
|
||||
*
|
||||
* @param {number} milliseconds - The number of milliseconds to wait the specified
|
||||
* {@code promise} to settle before automatically rejecting the returned
|
||||
* {@code Promise}.
|
||||
* @param {Promise} promise - The {@code Promise} for which automatic rejecting
|
||||
* after the specified timeout is to be implemented.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function timeout(milliseconds: number, promise: Promise<any>): Promise<Object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('408'));
|
||||
|
||||
return;
|
||||
}, milliseconds);
|
||||
|
||||
promise.then(resolve, reject);
|
||||
});
|
||||
}
|
||||
27
react/features/virtual-background/hooks.ts
Normal file
27
react/features/virtual-background/hooks.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isScreenVideoShared } from '../screen-share/functions';
|
||||
|
||||
import VideoBackgroundButton from './components/VideoBackgroundButton';
|
||||
import { checkBlurSupport, checkVirtualBackgroundEnabled } from './functions';
|
||||
|
||||
const virtualBackground = {
|
||||
key: 'select-background',
|
||||
Content: VideoBackgroundButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the virtual background button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useVirtualBackgroundButton() {
|
||||
const _checkBlurSupport = checkBlurSupport();
|
||||
const _isScreenVideoShared = useSelector(isScreenVideoShared);
|
||||
const _checkVirtualBackgroundEnabled = useSelector(checkVirtualBackgroundEnabled);
|
||||
|
||||
if (_checkBlurSupport && !_isScreenVideoShared && _checkVirtualBackgroundEnabled) {
|
||||
return virtualBackground;
|
||||
}
|
||||
}
|
||||
3
react/features/virtual-background/logger.ts
Normal file
3
react/features/virtual-background/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/virtual-background');
|
||||
54
react/features/virtual-background/reducer.ts
Normal file
54
react/features/virtual-background/reducer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes';
|
||||
|
||||
const STORE_NAME = 'features/virtual-background';
|
||||
|
||||
export interface IVirtualBackground {
|
||||
backgroundEffectEnabled?: boolean;
|
||||
backgroundType?: string;
|
||||
blurValue?: number;
|
||||
selectedThumbnail?: string;
|
||||
virtualSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces redux actions which activate/deactivate virtual background image, or
|
||||
* indicate if the virtual image background is activated/deactivated. The
|
||||
* backgroundEffectEnabled flag indicate if virtual background effect is activated.
|
||||
*
|
||||
* @param {State} state - The current redux state.
|
||||
* @param {Action} action - The redux action to reduce.
|
||||
* @param {string} action.type - The type of the redux action to reduce..
|
||||
* @returns {State} The next redux state that is the result of reducing the
|
||||
* specified action.
|
||||
*/
|
||||
ReducerRegistry.register<IVirtualBackground>(STORE_NAME, (state = {}, action): IVirtualBackground => {
|
||||
const { virtualSource, backgroundEffectEnabled, blurValue, backgroundType, selectedThumbnail } = action;
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature {@code virtual-background}.
|
||||
*/
|
||||
PersistenceRegistry.register(STORE_NAME);
|
||||
|
||||
switch (action.type) {
|
||||
case SET_VIRTUAL_BACKGROUND: {
|
||||
return {
|
||||
...state,
|
||||
virtualSource,
|
||||
blurValue,
|
||||
backgroundType,
|
||||
selectedThumbnail
|
||||
};
|
||||
}
|
||||
case BACKGROUND_ENABLED: {
|
||||
return {
|
||||
...state,
|
||||
backgroundEffectEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user