Pārlūkot izejas kodu

feat(virtual-background) add virtual background preview

Also enable background selection while muted.
master
Tudor D. Pop 4 gadus atpakaļ
vecāks
revīzija
9ef984ca3d
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 31
- 4
css/modals/virtual-background/_virtual-background.scss Parādīt failu

@@ -118,6 +118,13 @@
118 118
 
119 119
 .modal-dialog-form .virtual-background-loading {
120 120
     overflow: hidden;
121
+    position: fixed;
122
+    left: 50%;
123
+    margin-top: 10px;
124
+    transform: translateX(-50%);
125
+}
126
+.modal-dialog-form .video-preview {
127
+    height: 250px;
121 128
 }
122 129
 .file-upload-btn {
123 130
     display: none;
@@ -126,6 +133,7 @@
126 133
     font-size: 14px;
127 134
     font-weight: 600;
128 135
     line-height: 20px;
136
+    margin-top: 16px;
129 137
     margin-bottom: 8px;
130 138
     color: #669AEC;
131 139
     display: inline-flex;
@@ -150,10 +158,29 @@
150 158
     position: relative;
151 159
 }
152 160
 
153
-.loading-content-text{
154
-  margin-right: 15px;
155
-}
156
-
157 161
 .add-background{
158 162
     margin-right: 8px;
159 163
 }
164
+
165
+.apply-background-btn{
166
+    margin-top: 16px;
167
+    float: right;
168
+ }
169
+
170
+ .video-background-preview-entry{
171
+    height: 250px;
172
+    margin-bottom: 8px;
173
+    width: 572px;
174
+    position: fixed;
175
+    z-index: 2;
176
+    @media (min-width: 432px) and (max-width: 632px) {
177
+        width: 340px;
178
+    }
179
+ }
180
+
181
+ .video-preview-loader{
182
+     position: fixed;
183
+     left: 50%;
184
+     top: 35%;
185
+     transform: translate(-50%,-35%);
186
+ }

+ 1
- 1
lang/main.json Parādīt failu

@@ -339,12 +339,12 @@
339 339
         "title": "Embed this meeting"
340 340
     },
341 341
     "virtualBackground": {
342
+        "apply": "Apply",
342 343
         "title": "Virtual backgrounds",
343 344
         "blur": "Blur",
344 345
         "slightBlur": "Slight Blur",
345 346
         "removeBackground": "Remove background",
346 347
         "addBackground": "Add background",
347
-        "pleaseWait": "Please wait...",
348 348
         "none": "None"
349 349
     },
350 350
     "feedback": {

+ 10
- 1
react/features/prejoin/actions.js Parādīt failu

@@ -6,12 +6,14 @@ import uuid from 'uuid';
6 6
 
7 7
 import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
8 8
 import { createLocalTrack } from '../base/lib-jitsi-meet';
9
+import { isVideoMutedByUser } from '../base/media';
9 10
 import {
10 11
     getLocalAudioTrack,
11 12
     getLocalVideoTrack,
12 13
     trackAdded,
13 14
     replaceLocalTrack
14 15
 } from '../base/tracks';
16
+import { createLocalTracksF } from '../base/tracks/functions';
15 17
 import { openURLInBrowser } from '../base/util';
16 18
 import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
17 19
 import { showErrorNotification } from '../notifications';
@@ -309,10 +311,17 @@ export function replaceVideoTrackById(deviceId: Object) {
309 311
     return async (dispatch: Function, getState: Function) => {
310 312
         try {
311 313
             const tracks = getState()['features/base/tracks'];
312
-            const newTrack = await createLocalTrack('video', deviceId);
314
+            const wasVideoMuted = isVideoMutedByUser(getState());
315
+            const [ newTrack ] = await createLocalTracksF(
316
+                { cameraDeviceId: deviceId,
317
+                    devices: [ 'video' ] },
318
+                { dispatch,
319
+                    getState }
320
+            );
313 321
             const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
314 322
 
315 323
             dispatch(replaceLocalTrack(oldTrack, newTrack));
324
+            wasVideoMuted && newTrack.mute();
316 325
         } catch (err) {
317 326
             dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
318 327
             logger.log('Error replacing video track', err);

+ 12
- 11
react/features/virtual-background/actions.js Parādīt failu

@@ -1,6 +1,5 @@
1 1
 // @flow
2 2
 
3
-import { getLocalVideoTrack } from '../base/tracks';
4 3
 import { createVirtualBackgroundEffect } from '../stream-effects/virtual-background';
5 4
 
6 5
 import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes';
@@ -10,26 +9,28 @@ import logger from './logger';
10 9
  * Signals the local participant activate the virtual background video or not.
11 10
  *
12 11
  * @param {Object} options - Represents the virtual background setted options.
12
+ * @param {Object} jitsiTrack - Represents the jitsi track that will have backgraund effect applied.
13 13
  * @returns {Promise}
14 14
  */
15
-export function toggleBackgroundEffect(options: Object) {
15
+export function toggleBackgroundEffect(options: Object, jitsiTrack: Object) {
16 16
     return async function(dispatch: Object => Object, getState: () => any) {
17 17
         await dispatch(backgroundEnabled(options.enabled));
18 18
         await dispatch(setVirtualBackground(options));
19 19
         const state = getState();
20
-        const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']);
21 20
         const virtualBackground = state['features/virtual-background'];
22 21
 
23
-        try {
24
-            if (options.enabled) {
25
-                await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground));
26
-            } else {
27
-                await jitsiTrack.setEffect(undefined);
22
+        if (jitsiTrack) {
23
+            try {
24
+                if (options.enabled) {
25
+                    await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground));
26
+                } else {
27
+                    await jitsiTrack.setEffect(undefined);
28
+                    dispatch(backgroundEnabled(false));
29
+                }
30
+            } catch (error) {
28 31
                 dispatch(backgroundEnabled(false));
32
+                logger.error('Error on apply background effect:', error);
29 33
             }
30
-        } catch (error) {
31
-            dispatch(backgroundEnabled(false));
32
-            logger.error('Error on apply backgroun effect:', error);
33 34
         }
34 35
     };
35 36
 }

+ 1
- 20
react/features/virtual-background/components/VideoBackgroundButton.js Parādīt failu

@@ -6,7 +6,6 @@ import { IconVirtualBackground } from '../../base/icons';
6 6
 import { connect } from '../../base/redux';
7 7
 import { AbstractButton } from '../../base/toolbox/components';
8 8
 import type { AbstractButtonProps } from '../../base/toolbox/components';
9
-import { isLocalCameraTrackMuted } from '../../base/tracks';
10 9
 
11 10
 import { VirtualBackgroundDialog } from './index';
12 11
 
@@ -20,11 +19,6 @@ type Props = AbstractButtonProps & {
20 19
      */
21 20
     _isBackgroundEnabled: boolean,
22 21
 
23
-    /**
24
-     * Whether video is currently muted or not.
25
-     */
26
-    _videoMuted: boolean,
27
-
28 22
     /**
29 23
      * The redux {@code dispatch} function.
30 24
      */
@@ -63,17 +57,6 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
63 57
     _isToggled() {
64 58
         return this.props._isBackgroundEnabled;
65 59
     }
66
-
67
-    /**
68
-     * Returns {@code boolean} value indicating if disabled state is
69
-     * enabled or not.
70
-     *
71
-     * @protected
72
-     * @returns {boolean}
73
-     */
74
-    _isDisabled() {
75
-        return this.props._videoMuted;
76
-    }
77 60
 }
78 61
 
79 62
 /**
@@ -87,11 +70,9 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
87 70
  * }}
88 71
  */
89 72
 function _mapStateToProps(state): Object {
90
-    const tracks = state['features/base/tracks'];
91 73
 
92 74
     return {
93
-        _isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled),
94
-        _videoMuted: isLocalCameraTrackMuted(tracks)
75
+        _isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled)
95 76
     };
96 77
 }
97 78
 

+ 59
- 58
react/features/virtual-background/components/VirtualBackgroundDialog.js Parādīt failu

@@ -5,14 +5,17 @@ import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
5 5
 import React, { useState, useEffect } from 'react';
6 6
 import uuid from 'uuid';
7 7
 
8
-import { Dialog } from '../../base/dialog';
8
+import { Dialog, hideDialog } from '../../base/dialog';
9 9
 import { translate } from '../../base/i18n';
10 10
 import { Icon, IconCloseSmall, IconPlusCircle } from '../../base/icons';
11 11
 import { connect } from '../../base/redux';
12
+import { getLocalVideoTrack } from '../../base/tracks';
12 13
 import { toggleBackgroundEffect } from '../actions';
13 14
 import { resizeImage, toDataURL } from '../functions';
14 15
 import logger from '../logger';
15 16
 
17
+import VirtualBackgroundPreview from './VirtualBackgroundPreview';
18
+
16 19
 // The limit of virtual background uploads is 24. When the number
17 20
 // of uploads is 25 we trigger the deleteStoredImage function to delete
18 21
 // the first/oldest uploaded background.
@@ -49,6 +52,11 @@ const images = [
49 52
 ];
50 53
 type Props = {
51 54
 
55
+    /**
56
+     * Returns the jitsi track that will have backgraund effect applied.
57
+     */
58
+    _jitsiTrack: Object,
59
+
52 60
     /**
53 61
      * Returns the selected thumbnail identifier.
54 62
      */
@@ -70,7 +78,8 @@ type Props = {
70 78
  *
71 79
  * @returns {ReactElement}
72 80
  */
73
-function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
81
+function VirtualBackground({ _jitsiTrack, _selectedThumbnail, dispatch, t }: Props) {
82
+    const [ options, setOptions ] = useState({});
74 83
     const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
75 84
     const [ storedImages, setStoredImages ] = useState((localImages && JSON.parse(localImages)) || []);
76 85
     const [ loading, isloading ] = useState(false);
@@ -95,55 +104,39 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
95 104
     }, [ storedImages ]);
96 105
 
97 106
     const enableBlur = async (blurValue, selection) => {
98
-        isloading(true);
99
-        await dispatch(
100
-            toggleBackgroundEffect({
101
-                backgroundType: 'blur',
102
-                enabled: true,
103
-                blurValue,
104
-                selectedThumbnail: selection
105
-            })
106
-        );
107
-        isloading(false);
107
+        setOptions({
108
+            backgroundType: 'blur',
109
+            enabled: true,
110
+            blurValue,
111
+            selectedThumbnail: selection
112
+        });
108 113
     };
109 114
 
110 115
     const removeBackground = async () => {
111
-        isloading(true);
112
-        await dispatch(
113
-            toggleBackgroundEffect({
114
-                enabled: false,
115
-                selectedThumbnail: 'none'
116
-            })
117
-        );
118
-        isloading(false);
116
+        setOptions({
117
+            enabled: false,
118
+            selectedThumbnail: 'none'
119
+        });
119 120
     };
120 121
 
121 122
     const setUploadedImageBackground = async image => {
122
-        isloading(true);
123
-        await dispatch(
124
-            toggleBackgroundEffect({
125
-                backgroundType: 'image',
126
-                enabled: true,
127
-                url: image.src,
128
-                selectedThumbnail: image.id
129
-            })
130
-        );
131
-        isloading(false);
123
+        setOptions({
124
+            backgroundType: 'image',
125
+            enabled: true,
126
+            url: image.src,
127
+            selectedThumbnail: image.id
128
+        });
132 129
     };
133 130
 
134 131
     const setImageBackground = async image => {
135
-        isloading(true);
136 132
         const url = await toDataURL(image.src);
137 133
 
138
-        await dispatch(
139
-            toggleBackgroundEffect({
140
-                backgroundType: 'image',
141
-                enabled: true,
142
-                url,
143
-                selectedThumbnail: image.id
144
-            })
145
-        );
146
-        isloading(false);
134
+        setOptions({
135
+            backgroundType: 'image',
136
+            enabled: true,
137
+            url,
138
+            selectedThumbnail: image.id
139
+        });
147 140
     };
148 141
 
149 142
     const uploadImage = async imageFile => {
@@ -154,7 +147,6 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
154 147
             const url = await resizeImage(reader.result);
155 148
             const uuId = uuid.v4();
156 149
 
157
-            isloading(true);
158 150
             setStoredImages([
159 151
                 ...storedImages,
160 152
                 {
@@ -162,15 +154,12 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
162 154
                     src: url
163 155
                 }
164 156
             ]);
165
-            await dispatch(
166
-                toggleBackgroundEffect({
167
-                    backgroundType: 'image',
168
-                    enabled: true,
169
-                    url,
170
-                    selectedThumbnail: uuId
171
-                })
172
-            );
173
-            isloading(false);
157
+            setOptions({
158
+                backgroundType: 'image',
159
+                enabled: true,
160
+                url,
161
+                selectedThumbnail: uuId
162
+            });
174 163
         };
175 164
         reader.onerror = () => {
176 165
             isloading(false);
@@ -178,15 +167,24 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
178 167
         };
179 168
     };
180 169
 
170
+    const applyVirtualBackground = async () => {
171
+        isloading(true);
172
+        await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
173
+        await isloading(false);
174
+        dispatch(hideDialog());
175
+    };
176
+
181 177
     return (
182 178
         <Dialog
183
-            hideCancelButton = { true }
184
-            submitDisabled = { true }
179
+            hideCancelButton = { false }
180
+            okKey = { 'virtualBackground.apply' }
181
+            onSubmit = { applyVirtualBackground }
182
+            submitDisabled = { !options || loading }
185 183
             titleKey = { 'virtualBackground.title' }
186 184
             width = '640px'>
185
+            <VirtualBackgroundPreview options = { options } />
187 186
             {loading ? (
188 187
                 <div className = 'virtual-background-loading'>
189
-                    <span className = 'loading-content-text'>{t('virtualBackground.pleaseWait')}</span>
190 188
                     <Spinner
191 189
                         isCompleting = { false }
192 190
                         size = 'medium' />
@@ -227,7 +225,11 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
227 225
                         </div>
228 226
                         {images.map((image, index) => (
229 227
                             <img
230
-                                className = { _selectedThumbnail === image.id ? 'thumbnail-selected' : 'thumbnail' }
228
+                                className = {
229
+                                    options.selectedThumbnail === image.id || _selectedThumbnail === image.id
230
+                                        ? 'thumbnail-selected'
231
+                                        : 'thumbnail'
232
+                                }
231 233
                                 key = { index }
232 234
                                 onClick = { () => setImageBackground(image) }
233 235
                                 onError = { event => event.target.style.display = 'none' }
@@ -262,13 +264,12 @@ function VirtualBackground({ _selectedThumbnail, dispatch, t }: Props) {
262 264
  *
263 265
  * @param {Object} state - The Redux state.
264 266
  * @private
265
- * @returns {{
266
- *     _selectedThumbnail: string
267
- * }}
267
+ * @returns {{Props}}
268 268
  */
269 269
 function _mapStateToProps(state): Object {
270 270
     return {
271
-        _selectedThumbnail: state['features/virtual-background'].selectedThumbnail
271
+        _selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
272
+        _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
272 273
     };
273 274
 }
274 275
 

+ 226
- 0
react/features/virtual-background/components/VirtualBackgroundPreview.js Parādīt failu

@@ -0,0 +1,226 @@
1
+// @flow
2
+
3
+import Spinner from '@atlaskit/spinner';
4
+import React, { PureComponent } from 'react';
5
+
6
+import { translate } from '../../base/i18n';
7
+import Video from '../../base/media/components/Video';
8
+import { connect, equals } from '../../base/redux';
9
+import { getCurrentCameraDeviceId } from '../../base/settings';
10
+import { createLocalTracksF } from '../../base/tracks/functions';
11
+import { toggleBackgroundEffect } from '../actions';
12
+
13
+const videoClassName = 'video-preview-video flipVideoX';
14
+
15
+/**
16
+ * The type of the React {@code PureComponent} props of {@link VirtualBackgroundPreview}.
17
+ */
18
+export type Props = {
19
+
20
+    /**
21
+     * The deviceId of the camera device currently being used.
22
+     */
23
+    _currentCameraDeviceId: string,
24
+
25
+    /**
26
+     * The redux {@code dispatch} function.
27
+     */
28
+    dispatch: Function,
29
+
30
+    /**
31
+     * Represents the virtual background setted options.
32
+     */
33
+    options: Object,
34
+
35
+    /**
36
+     * Invoked to obtain translated strings.
37
+     */
38
+    t: Function
39
+};
40
+
41
+/**
42
+ * The type of the React {@code Component} state of {@link VirtualBackgroundPreview}.
43
+ */
44
+type State = {
45
+
46
+    /**
47
+     * Loader activated on setting virtual background.
48
+     */
49
+    loading: boolean,
50
+
51
+    /**
52
+     * Activate the selected device camera only.
53
+     */
54
+    jitsiTrack: Object
55
+};
56
+
57
+/**
58
+ * Implements a React {@link PureComponent} which displays the virtual
59
+ * background preview.
60
+ *
61
+ * @extends PureComponent
62
+ */
63
+class VirtualBackgroundPreview extends PureComponent<Props, State> {
64
+    _componentWasUnmounted: boolean;
65
+
66
+    /**
67
+     * Initializes a new {@code VirtualBackgroundPreview} instance.
68
+     *
69
+     * @param {Object} props - The read-only properties with which the new
70
+     * instance is to be initialized.
71
+     */
72
+    constructor(props) {
73
+        super(props);
74
+
75
+        this.state = {
76
+            loading: false,
77
+            jitsiTrack: null
78
+        };
79
+    }
80
+
81
+    /**
82
+     * Creates and updates the track data.
83
+     *
84
+     * @returns {void}
85
+     */
86
+    async _setTracks() {
87
+        const [ jitsiTrack ] = await createLocalTracksF({
88
+            cameraDeviceId: this.props._currentCameraDeviceId,
89
+            devices: [ 'video' ]
90
+        });
91
+
92
+        // In case the component gets unmounted before the tracks are created
93
+        // avoid a leak by not setting the state
94
+        if (this._componentWasUnmounted) {
95
+            return;
96
+        }
97
+        this.setState({
98
+            jitsiTrack
99
+        });
100
+    }
101
+
102
+    /**
103
+     * Apply background effect on video preview.
104
+     *
105
+     * @returns {Promise}
106
+     */
107
+    async _applyBackgroundEffect() {
108
+        this.setState({ loading: true });
109
+        await this.props.dispatch(toggleBackgroundEffect(this.props.options, this.state.jitsiTrack));
110
+        this.setState({ loading: false });
111
+    }
112
+
113
+    /**
114
+     * Apply video preview loader.
115
+     *
116
+     * @returns {Promise}
117
+     */
118
+    _loadVideoPreview() {
119
+        return (
120
+            <div className = 'video-preview-loader'>
121
+                <Spinner
122
+                    invertColor = { true }
123
+                    isCompleting = { false }
124
+                    size = { 'large' } />
125
+            </div>
126
+        );
127
+    }
128
+
129
+    /**
130
+     * Renders a preview entry.
131
+     *
132
+     * @param {Object} data - The track data.
133
+     * @returns {React$Node}
134
+     */
135
+    _renderPreviewEntry(data) {
136
+        const { t } = this.props;
137
+        const className = 'video-background-preview-entry';
138
+
139
+        if (this.state.loading) {
140
+            return this._loadVideoPreview();
141
+        }
142
+        if (!data) {
143
+            return (
144
+                <div
145
+                    className = { className }
146
+                    video-preview-container = { true }>
147
+                    <div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
148
+                </div>
149
+            );
150
+        }
151
+        const props: Object = {
152
+            className
153
+        };
154
+
155
+        return (
156
+            <div { ...props }>
157
+                <Video
158
+                    className = { videoClassName }
159
+                    playsinline = { true }
160
+                    videoTrack = {{ jitsiTrack: data }} />
161
+            </div>
162
+        );
163
+    }
164
+
165
+    /**
166
+     * Implements React's {@link Component#componentDidMount}.
167
+     *
168
+     * @inheritdoc
169
+     */
170
+    componentDidMount() {
171
+        this._setTracks();
172
+    }
173
+
174
+    /**
175
+     * Implements React's {@link Component#componentWillUnmount}.
176
+     *
177
+     * @inheritdoc
178
+     */
179
+    componentWillUnmount() {
180
+        this._componentWasUnmounted = true;
181
+    }
182
+
183
+    /**
184
+     * Implements React's {@link Component#componentDidUpdate}.
185
+     *
186
+     * @inheritdoc
187
+     */
188
+    async componentDidUpdate(prevProps) {
189
+        if (!equals(this.props._currentCameraDeviceId, prevProps._currentCameraDeviceId)) {
190
+            this._setTracks();
191
+        }
192
+        if (!equals(this.props.options, prevProps.options)) {
193
+            this._applyBackgroundEffect();
194
+        }
195
+    }
196
+
197
+    /**
198
+     * Implements React's {@link Component#render}.
199
+     *
200
+     * @inheritdoc
201
+     */
202
+    render() {
203
+        const { jitsiTrack } = this.state;
204
+
205
+        return jitsiTrack
206
+            ? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
207
+            : <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
208
+        ;
209
+    }
210
+}
211
+
212
+/**
213
+ * Maps (parts of) the redux state to the associated props for the
214
+ * {@code VirtualBackgroundPreview} component.
215
+ *
216
+ * @param {Object} state - The Redux state.
217
+ * @private
218
+ * @returns {{Props}}
219
+ */
220
+function _mapStateToProps(state): Object {
221
+    return {
222
+        _currentCameraDeviceId: getCurrentCameraDeviceId(state)
223
+    };
224
+}
225
+
226
+export default translate(connect(_mapStateToProps)(VirtualBackgroundPreview));

Notiek ielāde…
Atcelt
Saglabāt