feat(virtual-background) add virtual background preview
Also enable background selection while muted.
This commit is contained in:
parent
2f51d9fd3e
commit
9ef984ca3d
|
@ -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%);
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
Loading…
Reference in New Issue