feat(virtual-background) add virtual background preview

Also enable background selection while muted.
This commit is contained in:
Tudor D. Pop 2021-05-06 09:54:23 +03:00 committed by GitHub
parent 2f51d9fd3e
commit 9ef984ca3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 340 additions and 95 deletions

View File

@ -118,6 +118,13 @@
.modal-dialog-form .virtual-background-loading {
overflow: hidden;
position: fixed;
left: 50%;
margin-top: 10px;
transform: translateX(-50%);
}
.modal-dialog-form .video-preview {
height: 250px;
}
.file-upload-btn {
display: none;
@ -126,6 +133,7 @@
font-size: 14px;
font-weight: 600;
line-height: 20px;
margin-top: 16px;
margin-bottom: 8px;
color: #669AEC;
display: inline-flex;
@ -150,10 +158,29 @@
position: relative;
}
.loading-content-text{
margin-right: 15px;
}
.add-background{
margin-right: 8px;
}
.apply-background-btn{
margin-top: 16px;
float: right;
}
.video-background-preview-entry{
height: 250px;
margin-bottom: 8px;
width: 572px;
position: fixed;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
width: 340px;
}
}
.video-preview-loader{
position: fixed;
left: 50%;
top: 35%;
transform: translate(-50%,-35%);
}

View File

@ -339,12 +339,12 @@
"title": "Embed this meeting"
},
"virtualBackground": {
"apply": "Apply",
"title": "Virtual backgrounds",
"blur": "Blur",
"slightBlur": "Slight Blur",
"removeBackground": "Remove background",
"addBackground": "Add background",
"pleaseWait": "Please wait...",
"none": "None"
},
"feedback": {

View File

@ -6,12 +6,14 @@ import uuid from 'uuid';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { createLocalTrack } from '../base/lib-jitsi-meet';
import { isVideoMutedByUser } from '../base/media';
import {
getLocalAudioTrack,
getLocalVideoTrack,
trackAdded,
replaceLocalTrack
} from '../base/tracks';
import { createLocalTracksF } from '../base/tracks/functions';
import { openURLInBrowser } from '../base/util';
import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import { showErrorNotification } from '../notifications';
@ -309,10 +311,17 @@ export function replaceVideoTrackById(deviceId: Object) {
return async (dispatch: Function, getState: Function) => {
try {
const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('video', deviceId);
const wasVideoMuted = isVideoMutedByUser(getState());
const [ newTrack ] = await createLocalTracksF(
{ cameraDeviceId: deviceId,
devices: [ 'video' ] },
{ dispatch,
getState }
);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
dispatch(replaceLocalTrack(oldTrack, newTrack));
wasVideoMuted && newTrack.mute();
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
logger.log('Error replacing video track', err);

View File

@ -1,6 +1,5 @@
// @flow
import { getLocalVideoTrack } from '../base/tracks';
import { createVirtualBackgroundEffect } from '../stream-effects/virtual-background';
import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes';
@ -10,26 +9,28 @@ import logger from './logger';
* Signals the local participant activate the virtual background video or not.
*
* @param {Object} options - Represents the virtual background setted options.
* @param {Object} jitsiTrack - Represents the jitsi track that will have backgraund effect applied.
* @returns {Promise}
*/
export function toggleBackgroundEffect(options: Object) {
export function toggleBackgroundEffect(options: Object, jitsiTrack: Object) {
return async function(dispatch: Object => Object, getState: () => any) {
await dispatch(backgroundEnabled(options.enabled));
await dispatch(setVirtualBackground(options));
const state = getState();
const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']);
const virtualBackground = state['features/virtual-background'];
try {
if (options.enabled) {
await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground));
} else {
await jitsiTrack.setEffect(undefined);
if (jitsiTrack) {
try {
if (options.enabled) {
await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground));
} else {
await jitsiTrack.setEffect(undefined);
dispatch(backgroundEnabled(false));
}
} catch (error) {
dispatch(backgroundEnabled(false));
logger.error('Error on apply background effect:', error);
}
} catch (error) {
dispatch(backgroundEnabled(false));
logger.error('Error on apply backgroun effect:', error);
}
};
}

View File

@ -6,7 +6,6 @@ import { IconVirtualBackground } from '../../base/icons';
import { connect } from '../../base/redux';
import { AbstractButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { isLocalCameraTrackMuted } from '../../base/tracks';
import { VirtualBackgroundDialog } from './index';
@ -20,11 +19,6 @@ type Props = AbstractButtonProps & {
*/
_isBackgroundEnabled: boolean,
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
@ -63,17 +57,6 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
_isToggled() {
return this.props._isBackgroundEnabled;
}
/**
* Returns {@code boolean} value indicating if disabled state is
* enabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._videoMuted;
}
}
/**
@ -87,11 +70,9 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
* }}
*/
function _mapStateToProps(state): Object {
const tracks = state['features/base/tracks'];
return {
_isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled),
_videoMuted: isLocalCameraTrackMuted(tracks)
_isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled)
};
}

View File

@ -5,14 +5,17 @@ import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect } from 'react';
import uuid from 'uuid';
import { Dialog } from '../../base/dialog';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { Icon, IconCloseSmall, IconPlusCircle } from '../../base/icons';
import { connect } from '../../base/redux';
import { getLocalVideoTrack } from '../../base/tracks';
import { toggleBackgroundEffect } from '../actions';
import { resizeImage, toDataURL } from '../functions';
import logger from '../logger';
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
// 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.
@ -49,6 +52,11 @@ const images = [
];
type Props = {
/**
* Returns the jitsi track that will have backgraund effect applied.
*/
_jitsiTrack: Object,
/**
* Returns the selected thumbnail identifier.
*/
@ -70,7 +78,8 @@ type Props = {
*
* @returns {ReactElement}
*/
function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
function VirtualBackground({ _jitsiTrack, _selectedThumbnail, dispatch, t }: Props) {
const [ options, setOptions ] = useState({});
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState((localImages && JSON.parse(localImages)) || []);
const [ loading, isloading ] = useState(false);
@ -95,55 +104,39 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
}, [ storedImages ]);
const enableBlur = async (blurValue, selection) => {
isloading(true);
await dispatch(
toggleBackgroundEffect({
backgroundType: 'blur',
enabled: true,
blurValue,
selectedThumbnail: selection
})
);
isloading(false);
setOptions({
backgroundType: 'blur',
enabled: true,
blurValue,
selectedThumbnail: selection
});
};
const removeBackground = async () => {
isloading(true);
await dispatch(
toggleBackgroundEffect({
enabled: false,
selectedThumbnail: 'none'
})
);
isloading(false);
setOptions({
enabled: false,
selectedThumbnail: 'none'
});
};
const setUploadedImageBackground = async image => {
isloading(true);
await dispatch(
toggleBackgroundEffect({
backgroundType: 'image',
enabled: true,
url: image.src,
selectedThumbnail: image.id
})
);
isloading(false);
setOptions({
backgroundType: 'image',
enabled: true,
url: image.src,
selectedThumbnail: image.id
});
};
const setImageBackground = async image => {
isloading(true);
const url = await toDataURL(image.src);
await dispatch(
toggleBackgroundEffect({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
})
);
isloading(false);
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
});
};
const uploadImage = async imageFile => {
@ -154,7 +147,6 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
const url = await resizeImage(reader.result);
const uuId = uuid.v4();
isloading(true);
setStoredImages([
...storedImages,
{
@ -162,15 +154,12 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
src: url
}
]);
await dispatch(
toggleBackgroundEffect({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: uuId
})
);
isloading(false);
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: uuId
});
};
reader.onerror = () => {
isloading(false);
@ -178,15 +167,24 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
};
};
const applyVirtualBackground = async () => {
isloading(true);
await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
await isloading(false);
dispatch(hideDialog());
};
return (
<Dialog
hideCancelButton = { true }
submitDisabled = { true }
hideCancelButton = { false }
okKey = { 'virtualBackground.apply' }
onSubmit = { applyVirtualBackground }
submitDisabled = { !options || loading }
titleKey = { 'virtualBackground.title' }
width = '640px'>
<VirtualBackgroundPreview options = { options } />
{loading ? (
<div className = 'virtual-background-loading'>
<span className = 'loading-content-text'>{t('virtualBackground.pleaseWait')}</span>
<Spinner
isCompleting = { false }
size = 'medium' />
@ -227,7 +225,11 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
</div>
{images.map((image, index) => (
<img
className = { _selectedThumbnail === image.id ? 'thumbnail-selected' : 'thumbnail' }
className = {
options.selectedThumbnail === image.id || _selectedThumbnail === image.id
? 'thumbnail-selected'
: 'thumbnail'
}
key = { index }
onClick = { () => setImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
@ -262,13 +264,12 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _selectedThumbnail: string
* }}
* @returns {{Props}}
*/
function _mapStateToProps(state): Object {
return {
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
};
}

View File

@ -0,0 +1,226 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React, { PureComponent } from 'react';
import { translate } from '../../base/i18n';
import Video from '../../base/media/components/Video';
import { connect, equals } from '../../base/redux';
import { getCurrentCameraDeviceId } from '../../base/settings';
import { createLocalTracksF } from '../../base/tracks/functions';
import { toggleBackgroundEffect } from '../actions';
const videoClassName = 'video-preview-video flipVideoX';
/**
* The type of the React {@code PureComponent} props of {@link VirtualBackgroundPreview}.
*/
export type Props = {
/**
* The deviceId of the camera device currently being used.
*/
_currentCameraDeviceId: string,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* Represents the virtual background setted options.
*/
options: Object,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link VirtualBackgroundPreview}.
*/
type State = {
/**
* Loader activated on setting virtual background.
*/
loading: boolean,
/**
* Activate the selected device camera only.
*/
jitsiTrack: Object
};
/**
* Implements a React {@link PureComponent} which displays the virtual
* background preview.
*
* @extends PureComponent
*/
class VirtualBackgroundPreview extends PureComponent<Props, State> {
_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) {
super(props);
this.state = {
loading: false,
jitsiTrack: null
};
}
/**
* Creates and updates the track data.
*
* @returns {void}
*/
async _setTracks() {
const [ jitsiTrack ] = await createLocalTracksF({
cameraDeviceId: this.props._currentCameraDeviceId,
devices: [ 'video' ]
});
// In case the component gets unmounted before the tracks are created
// avoid a leak by not setting the state
if (this._componentWasUnmounted) {
return;
}
this.setState({
jitsiTrack
});
}
/**
* Apply background effect on video preview.
*
* @returns {Promise}
*/
async _applyBackgroundEffect() {
this.setState({ loading: true });
await this.props.dispatch(toggleBackgroundEffect(this.props.options, this.state.jitsiTrack));
this.setState({ loading: false });
}
/**
* Apply video preview loader.
*
* @returns {Promise}
*/
_loadVideoPreview() {
return (
<div className = 'video-preview-loader'>
<Spinner
invertColor = { true }
isCompleting = { false }
size = { 'large' } />
</div>
);
}
/**
* Renders a preview entry.
*
* @param {Object} data - The track data.
* @returns {React$Node}
*/
_renderPreviewEntry(data) {
const { t } = this.props;
const className = 'video-background-preview-entry';
if (this.state.loading) {
return this._loadVideoPreview();
}
if (!data) {
return (
<div
className = { className }
video-preview-container = { true }>
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
</div>
);
}
const props: Object = {
className
};
return (
<div { ...props }>
<Video
className = { videoClassName }
playsinline = { true }
videoTrack = {{ jitsiTrack: data }} />
</div>
);
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._setTracks();
}
/**
* Implements React's {@link Component#componentWillUnmount}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._componentWasUnmounted = true;
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
async componentDidUpdate(prevProps) {
if (!equals(this.props._currentCameraDeviceId, prevProps._currentCameraDeviceId)) {
this._setTracks();
}
if (!equals(this.props.options, prevProps.options)) {
this._applyBackgroundEffect();
}
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { jitsiTrack } = this.state;
return jitsiTrack
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VirtualBackgroundPreview} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{Props}}
*/
function _mapStateToProps(state): Object {
return {
_currentCameraDeviceId: getCurrentCameraDeviceId(state)
};
}
export default translate(connect(_mapStateToProps)(VirtualBackgroundPreview));