瀏覽代碼

feat(dynamic-branding): Add branding option for virtual backgrounds

master
Vlad Piersec 4 年之前
父節點
當前提交
f9cc813e91

+ 4
- 0
config.js 查看文件

@@ -909,6 +909,10 @@ var config = {
909 909
     */
910 910
     // dynamicBrandingUrl: '',
911 911
 
912
+    // When true the user cannot add more images to be used as virtual background.
913
+    // Only the default ones from will be available.
914
+    // disableAddingBackgroundImages: false,
915
+
912 916
     // Sets the background transparency level. '0' is fully transparent, '1' is opaque.
913 917
     // backgroundAlpha: 1,
914 918
 

+ 1
- 0
react/features/app/middlewares.web.js 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 import '../authentication/middleware';
4 4
 import '../base/devices/middleware';
5
+import '../dynamic-branding/middleware';
5 6
 import '../e2ee/middleware';
6 7
 import '../external-api/middleware';
7 8
 import '../keyboard-shortcuts/middleware';

+ 1
- 0
react/features/base/config/configWhitelist.js 查看文件

@@ -84,6 +84,7 @@ export default [
84 84
     'disableAEC',
85 85
     'disableAGC',
86 86
     'disableAP',
87
+    'disableAddingBackgroundImages',
87 88
     'disableAudioLevels',
88 89
     'disableChatSmileys',
89 90
     'disableDeepLinking',

+ 3
- 3
react/features/dynamic-branding/functions.js 查看文件

@@ -7,8 +7,8 @@
7 7
  * @param {string} path - The URL path.
8 8
  * @returns {string}
9 9
  */
10
-export function extractFqnFromPath(path: string) {
11
-    const parts = path.split('/');
10
+export function extractFqnFromPath() {
11
+    const parts = window.location.pathname.split('/');
12 12
     const len = parts.length;
13 13
 
14 14
     return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
@@ -28,7 +28,7 @@ export function getDynamicBrandingUrl(state: Object) {
28 28
     }
29 29
 
30 30
     const baseUrl = state['features/base/config'].brandingDataUrl;
31
-    const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
31
+    const fqn = extractFqnFromPath();
32 32
 
33 33
     if (baseUrl && fqn) {
34 34
         return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;

+ 18
- 0
react/features/dynamic-branding/middleware.js 查看文件

@@ -0,0 +1,18 @@
1
+// @flow
2
+
3
+import { APP_WILL_MOUNT } from '../base/app';
4
+import { MiddlewareRegistry } from '../base/redux';
5
+
6
+import { fetchCustomBrandingData } from './actions';
7
+
8
+MiddlewareRegistry.register(store => next => action => {
9
+    switch (action.type) {
10
+    case APP_WILL_MOUNT: {
11
+
12
+        store.dispatch(fetchCustomBrandingData());
13
+        break;
14
+    }
15
+    }
16
+
17
+    return next(action);
18
+});

+ 41
- 3
react/features/dynamic-branding/reducer.js 查看文件

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import { ReducerRegistry } from '../base/redux';
4
+import { type Image } from '../virtual-background/constants';
4 5
 
5 6
 import {
6 7
     SET_DYNAMIC_BRANDING_DATA,
@@ -113,7 +114,15 @@ const DEFAULT_STATE = {
113 114
      * @public
114 115
      * @type {boolean}
115 116
      */
116
-    useDynamicBrandingData: false
117
+    useDynamicBrandingData: false,
118
+
119
+    /**
120
+     * An array of images to be used as virtual backgrounds instead of the default ones.
121
+     *
122
+     * @public
123
+     * @type {Array<Object>}
124
+     */
125
+    virtualBackgrounds: []
117 126
 };
118 127
 
119 128
 /**
@@ -131,7 +140,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
131 140
             inviteDomain,
132 141
             logoClickUrl,
133 142
             logoImageUrl,
134
-            premeetingBackground
143
+            premeetingBackground,
144
+            virtualBackgrounds
135 145
         } = action.value;
136 146
 
137 147
         return {
@@ -146,7 +156,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
146 156
             premeetingBackground,
147 157
             customizationFailed: false,
148 158
             customizationReady: true,
149
-            useDynamicBrandingData: true
159
+            useDynamicBrandingData: true,
160
+            virtualBackgrounds: formatImages(virtualBackgrounds || [])
150 161
         };
151 162
     }
152 163
     case SET_DYNAMIC_BRANDING_FAILED: {
@@ -166,3 +177,30 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
166 177
 
167 178
     return state;
168 179
 });
180
+
181
+/**
182
+ * Transforms the branding images into an array of Images objects ready
183
+ * to be used as virtual backgrounds.
184
+ *
185
+ * @param {Array<string>} images -
186
+ * @private
187
+ * @returns {{Props}}
188
+ */
189
+function formatImages(images: Array<string> | Array<Object>): Array<Image> {
190
+    return images.map((img, i) => {
191
+        let src;
192
+        let tooltip;
193
+
194
+        if (typeof img === 'object') {
195
+            ({ src, tooltip } = img);
196
+        } else {
197
+            src = img;
198
+        }
199
+
200
+        return {
201
+            id: `branding-${i}`,
202
+            src,
203
+            tooltip
204
+        };
205
+    });
206
+}

+ 1
- 1
react/features/feedback/actions.js 查看文件

@@ -131,7 +131,7 @@ export function sendJaasFeedbackMetadata(conference: Object, feedback: Object) {
131 131
             return Promise.resolve();
132 132
         }
133 133
 
134
-        const meetingFqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
134
+        const meetingFqn = extractFqnFromPath();
135 135
         const feedbackData = {
136 136
             ...feedback,
137 137
             sessionId: conference.sessionId,

+ 1
- 19
react/features/large-video/components/LargeVideo.web.js 查看文件

@@ -5,7 +5,6 @@ import React, { Component } from 'react';
5 5
 import { Watermarks } from '../../base/react';
6 6
 import { connect } from '../../base/redux';
7 7
 import { setColorAlpha } from '../../base/util';
8
-import { fetchCustomBrandingData } from '../../dynamic-branding';
9 8
 import { SharedVideo } from '../../shared-video/components/web';
10 9
 import { Captions } from '../../subtitles/';
11 10
 
@@ -28,11 +27,6 @@ type Props = {
28 27
      */
29 28
      _customBackgroundImageUrl: string,
30 29
 
31
-    /**
32
-     * Fetches the branding data.
33
-     */
34
-    _fetchCustomBrandingData: Function,
35
-
36 30
     /**
37 31
      * Prop that indicates whether the chat is open.
38 32
      */
@@ -52,14 +46,6 @@ type Props = {
52 46
  * @extends Component
53 47
  */
54 48
 class LargeVideo extends Component<Props> {
55
-    /**
56
-     * Implements React's {@link Component#componentDidMount}.
57
-     *
58
-     * @inheritdoc
59
-     */
60
-    componentDidMount() {
61
-        this.props._fetchCustomBrandingData();
62
-    }
63 49
 
64 50
     /**
65 51
      * Implements React's {@link Component#render()}.
@@ -167,8 +153,4 @@ function _mapStateToProps(state) {
167 153
     };
168 154
 }
169 155
 
170
-const _mapDispatchToProps = {
171
-    _fetchCustomBrandingData: fetchCustomBrandingData
172
-};
173
-
174
-export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);
156
+export default connect(_mapStateToProps)(LargeVideo);

+ 1
- 2
react/features/reactions/functions.any.js 查看文件

@@ -55,7 +55,6 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
55 55
     const { webhookProxyUrl: url } = state['features/base/config'];
56 56
     const { conference } = state['features/base/conference'];
57 57
     const { jwt } = state['features/base/jwt'];
58
-    const { locationURL } = state['features/base/connection'];
59 58
     const localParticipant = getLocalParticipant(state);
60 59
 
61 60
     const headers = {
@@ -65,7 +64,7 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
65 64
 
66 65
 
67 66
     const reqBody = {
68
-        meetingFqn: extractFqnFromPath(locationURL.pathname),
67
+        meetingFqn: extractFqnFromPath(),
69 68
         sessionId: conference.sessionId,
70 69
         submitted: Date.now(),
71 70
         reactions,

+ 125
- 0
react/features/virtual-background/components/UploadImageButton.js 查看文件

@@ -0,0 +1,125 @@
1
+// @flow
2
+
3
+import React, { useCallback, useRef } from 'react';
4
+import uuid from 'uuid';
5
+
6
+import { translate } from '../../base/i18n';
7
+import { Icon, IconPlusCircle } from '../../base/icons';
8
+import { VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
9
+import { resizeImage } from '../functions';
10
+import logger from '../logger';
11
+
12
+type Props = {
13
+
14
+    /**
15
+     * Callback used to set the 'loading' state of the parent component.
16
+     */
17
+    setLoading: Function,
18
+
19
+    /**
20
+     * Callback used to set the options.
21
+     */
22
+    setOptions: Function,
23
+
24
+    /**
25
+     * Callback used to set the storedImages array.
26
+     */
27
+    setStoredImages: Function,
28
+
29
+    /**
30
+     * A list of images locally stored.
31
+     */
32
+    storedImages: Array<Image>,
33
+
34
+    /**
35
+     * If a label should be displayed alongside the button.
36
+     */
37
+    showLabel: boolean,
38
+
39
+    /**
40
+     * Used for translation.
41
+     */
42
+    t: Function
43
+}
44
+
45
+/**
46
+ * Component used to upload an image.
47
+ *
48
+ * @param {Object} Props - The props of the component.
49
+ * @returns {React$Node}
50
+ */
51
+function UploadImageButton({
52
+    setLoading,
53
+    setOptions,
54
+    setStoredImages,
55
+    showLabel,
56
+    storedImages,
57
+    t
58
+}: Props) {
59
+    const uploadImageButton: Object = useRef(null);
60
+    const uploadImageKeyPress = useCallback(e => {
61
+        if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
62
+            e.preventDefault();
63
+            uploadImageButton.current.click();
64
+        }
65
+    }, [ uploadImageButton.current ]);
66
+
67
+
68
+    const uploadImage = useCallback(async e => {
69
+        const reader = new FileReader();
70
+        const imageFile = e.target.files;
71
+
72
+        reader.readAsDataURL(imageFile[0]);
73
+        reader.onload = async () => {
74
+            const url = await resizeImage(reader.result);
75
+            const uuId = uuid.v4();
76
+
77
+            setStoredImages([
78
+                ...storedImages,
79
+                {
80
+                    id: uuId,
81
+                    src: url
82
+                }
83
+            ]);
84
+            setOptions({
85
+                backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
86
+                enabled: true,
87
+                url,
88
+                selectedThumbnail: uuId
89
+            });
90
+        };
91
+        logger.info('New virtual background image uploaded!');
92
+
93
+        reader.onerror = () => {
94
+            setLoading(false);
95
+            logger.error('Failed to upload virtual image!');
96
+        };
97
+    }, [ storedImages ]);
98
+
99
+    return (
100
+        <>
101
+            {showLabel && <label
102
+                aria-label = { t('virtualBackground.uploadImage') }
103
+                className = 'file-upload-label'
104
+                htmlFor = 'file-upload'
105
+                onKeyPress = { uploadImageKeyPress }
106
+                tabIndex = { 0 } >
107
+                <Icon
108
+                    className = { 'add-background' }
109
+                    size = { 20 }
110
+                    src = { IconPlusCircle } />
111
+                {t('virtualBackground.addBackground')}
112
+            </label>}
113
+
114
+            <input
115
+                accept = 'image/*'
116
+                className = 'file-upload-btn'
117
+                id = 'file-upload'
118
+                onChange = { uploadImage }
119
+                ref = { uploadImageButton }
120
+                type = 'file' />
121
+        </>
122
+    );
123
+}
124
+
125
+export default translate(UploadImageButton);

+ 46
- 124
react/features/virtual-background/components/VirtualBackgroundDialog.js 查看文件

@@ -3,12 +3,11 @@
3 3
 import Spinner from '@atlaskit/spinner';
4 4
 import Bourne from '@hapi/bourne';
5 5
 import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
6
-import React, { useState, useEffect, useCallback, useRef } from 'react';
7
-import uuid from 'uuid';
6
+import React, { useState, useEffect, useCallback } from 'react';
8 7
 
9 8
 import { Dialog, hideDialog, openDialog } from '../../base/dialog';
10 9
 import { translate } from '../../base/i18n';
11
-import { Icon, IconCloseSmall, IconPlusCircle, IconShareDesktop } from '../../base/icons';
10
+import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
12 11
 import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
13 12
 import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
14 13
 import { VIDEO_TYPE } from '../../base/media';
@@ -18,62 +17,20 @@ import { Tooltip } from '../../base/tooltip';
18 17
 import { getLocalVideoTrack } from '../../base/tracks';
19 18
 import { showErrorNotification } from '../../notifications';
20 19
 import { toggleBackgroundEffect } from '../actions';
21
-import { VIRTUAL_BACKGROUND_TYPE } from '../constants';
22
-import { resizeImage, toDataURL } from '../functions';
20
+import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
21
+import { toDataURL } from '../functions';
23 22
 import logger from '../logger';
24 23
 
24
+import UploadImageButton from './UploadImageButton';
25 25
 import VirtualBackgroundPreview from './VirtualBackgroundPreview';
26 26
 
27
-
28
-type Image = {
29
-    tooltip?: string,
30
-    id: string,
31
-    src: string
32
-}
33
-
34
-// The limit of virtual background uploads is 24. When the number
35
-// of uploads is 25 we trigger the deleteStoredImage function to delete
36
-// the first/oldest uploaded background.
37
-const backgroundsLimit = 25;
38
-const images: Array<Image> = [
39
-    {
40
-        tooltip: 'image1',
41
-        id: '1',
42
-        src: 'images/virtual-background/background-1.jpg'
43
-    },
44
-    {
45
-        tooltip: 'image2',
46
-        id: '2',
47
-        src: 'images/virtual-background/background-2.jpg'
48
-    },
49
-    {
50
-        tooltip: 'image3',
51
-        id: '3',
52
-        src: 'images/virtual-background/background-3.jpg'
53
-    },
54
-    {
55
-        tooltip: 'image4',
56
-        id: '4',
57
-        src: 'images/virtual-background/background-4.jpg'
58
-    },
59
-    {
60
-        tooltip: 'image5',
61
-        id: '5',
62
-        src: 'images/virtual-background/background-5.jpg'
63
-    },
64
-    {
65
-        tooltip: 'image6',
66
-        id: '6',
67
-        src: 'images/virtual-background/background-6.jpg'
68
-    },
69
-    {
70
-        tooltip: 'image7',
71
-        id: '7',
72
-        src: 'images/virtual-background/background-7.jpg'
73
-    }
74
-];
75 27
 type Props = {
76 28
 
29
+    /**
30
+     * The list of Images to choose from.
31
+     */
32
+    _images: Array<Image>,
33
+
77 34
     /**
78 35
      * The current local flip x status.
79 36
      */
@@ -89,6 +46,11 @@ type Props = {
89 46
      */
90 47
     _selectedThumbnail: string,
91 48
 
49
+    /**
50
+     * If the upload button should be displayed or not.
51
+     */
52
+    _showUploadButton: boolean,
53
+
92 54
     /**
93 55
      * Returns the selected virtual background object.
94 56
      */
@@ -128,11 +90,15 @@ const onError = event => {
128 90
  */
129 91
 function _mapStateToProps(state): Object {
130 92
     const { localFlipX } = state['features/base/settings'];
93
+    const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
94
+    const hasBrandingImages = Boolean(dynamicBrandingImages.length);
131 95
 
132 96
     return {
133 97
         _localFlipX: Boolean(localFlipX),
98
+        _images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
134 99
         _virtualBackground: state['features/virtual-background'],
135 100
         _selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
101
+        _showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
136 102
         _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
137 103
     };
138 104
 }
@@ -145,9 +111,11 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg
145 111
  * @returns {ReactElement}
146 112
  */
147 113
 function VirtualBackground({
148
-    _localFlipX,
114
+    _images,
149 115
     _jitsiTrack,
116
+    _localFlipX,
150 117
     _selectedThumbnail,
118
+    _showUploadButton,
151 119
     _virtualBackground,
152 120
     dispatch,
153 121
     initialOptions,
@@ -158,7 +126,7 @@ function VirtualBackground({
158 126
     const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
159 127
     const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
160 128
     const [ loading, setLoading ] = useState(false);
161
-    const uploadImageButton: Object = useRef(null);
129
+
162 130
     const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
163 131
         ? _virtualBackground.virtualSource
164 132
         : null);
@@ -186,7 +154,7 @@ function VirtualBackground({
186 154
             // Preventing localStorage QUOTA_EXCEEDED_ERR
187 155
             err && setStoredImages(storedImages.slice(1));
188 156
         }
189
-        if (storedImages.length === backgroundsLimit) {
157
+        if (storedImages.length === BACKGROUNDS_LIMIT) {
190 158
             setStoredImages(storedImages.slice(1));
191 159
         }
192 160
     }, [ storedImages ]);
@@ -321,61 +289,27 @@ function VirtualBackground({
321 289
 
322 290
     const setImageBackground = useCallback(async e => {
323 291
         const imageId = e.currentTarget.getAttribute('data-imageid');
324
-        const image = images.find(img => img.id === imageId);
292
+        const image = _images.find(img => img.id === imageId);
325 293
 
326 294
         if (image) {
327
-            const url = await toDataURL(image.src);
328
-
329
-            setOptions({
330
-                backgroundType: 'image',
331
-                enabled: true,
332
-                url,
333
-                selectedThumbnail: image.id
334
-            });
335
-            logger.info('Image setted for virtual background preview!');
295
+            try {
296
+                const url = await toDataURL(image.src);
297
+
298
+                setOptions({
299
+                    backgroundType: 'image',
300
+                    enabled: true,
301
+                    url,
302
+                    selectedThumbnail: image.id
303
+                });
304
+                logger.info('Image set for virtual background preview!');
305
+            } catch (err) {
306
+                logger.error('Could not fetch virtual background image:', err);
307
+            }
336 308
 
337 309
             setLoading(false);
338 310
         }
339 311
     }, []);
340 312
 
341
-    const uploadImage = useCallback(async e => {
342
-        const reader = new FileReader();
343
-        const imageFile = e.target.files;
344
-
345
-        reader.readAsDataURL(imageFile[0]);
346
-        reader.onload = async () => {
347
-            const url = await resizeImage(reader.result);
348
-            const uuId = uuid.v4();
349
-
350
-            setStoredImages([
351
-                ...storedImages,
352
-                {
353
-                    id: uuId,
354
-                    src: url
355
-                }
356
-            ]);
357
-            setOptions({
358
-                backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
359
-                enabled: true,
360
-                url,
361
-                selectedThumbnail: uuId
362
-            });
363
-        };
364
-        logger.info('New virtual background image uploaded!');
365
-
366
-        reader.onerror = () => {
367
-            setLoading(false);
368
-            logger.error('Failed to upload virtual image!');
369
-        };
370
-    }, [ dispatch, storedImages ]);
371
-
372
-    const uploadImageKeyPress = useCallback(e => {
373
-        if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
374
-            e.preventDefault();
375
-            uploadImageButton.current.click();
376
-        }
377
-    }, [ uploadImageButton.current ]);
378
-
379 313
     const setImageBackgroundKeyPress = useCallback(e => {
380 314
         if (e.key === ' ' || e.key === 'Enter') {
381 315
             e.preventDefault();
@@ -448,25 +382,13 @@ function VirtualBackground({
448 382
                 </div>
449 383
             ) : (
450 384
                 <div>
451
-                    {previewIsLoaded && <label
452
-                        aria-label = { t('virtualBackground.uploadImage') }
453
-                        className = 'file-upload-label'
454
-                        htmlFor = 'file-upload'
455
-                        onKeyPress = { uploadImageKeyPress }
456
-                        tabIndex = { 0 } >
457
-                        <Icon
458
-                            className = { 'add-background' }
459
-                            size = { 20 }
460
-                            src = { IconPlusCircle } />
461
-                        {t('virtualBackground.addBackground')}
462
-                    </label> }
463
-                    <input
464
-                        accept = 'image/*'
465
-                        className = 'file-upload-btn'
466
-                        id = 'file-upload'
467
-                        onChange = { uploadImage }
468
-                        ref = { uploadImageButton }
469
-                        type = 'file' />
385
+                    {_showUploadButton
386
+                    && <UploadImageButton
387
+                        setLoading = { setLoading }
388
+                        setOptions = { setOptions }
389
+                        setStoredImages = { setStoredImages }
390
+                        showLabel = { previewIsLoaded }
391
+                        storedImages = { storedImages } />}
470 392
                     <div
471 393
                         className = 'virtual-background-dialog'
472 394
                         role = 'radiogroup'
@@ -535,7 +457,7 @@ function VirtualBackground({
535 457
                                     src = { IconShareDesktop } />
536 458
                             </div>
537 459
                         </Tooltip>
538
-                        {images.map(image => (
460
+                        {_images.map(image => (
539 461
                             <Tooltip
540 462
                                 content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
541 463
                                 key = { image.id }

+ 53
- 0
react/features/virtual-background/constants.js 查看文件

@@ -1,3 +1,5 @@
1
+// @flow
2
+
1 3
 /**
2 4
  * An enumeration of the different virtual background types.
3 5
  *
@@ -9,3 +11,54 @@ export const VIRTUAL_BACKGROUND_TYPE = {
9 11
     BLUR: 'blur',
10 12
     NONE: 'none'
11 13
 };
14
+
15
+
16
+export type Image = {
17
+    tooltip?: string,
18
+    id: string,
19
+    src: string
20
+}
21
+
22
+// The limit of virtual background uploads is 24. When the number
23
+// of uploads is 25 we trigger the deleteStoredImage function to delete
24
+// the first/oldest uploaded background.
25
+export const BACKGROUNDS_LIMIT = 25;
26
+
27
+
28
+export const IMAGES: Array<Image> = [
29
+    {
30
+        tooltip: 'image1',
31
+        id: '1',
32
+        src: 'images/virtual-background/background-1.jpg'
33
+    },
34
+    {
35
+        tooltip: 'image2',
36
+        id: '2',
37
+        src: 'images/virtual-background/background-2.jpg'
38
+    },
39
+    {
40
+        tooltip: 'image3',
41
+        id: '3',
42
+        src: 'images/virtual-background/background-3.jpg'
43
+    },
44
+    {
45
+        tooltip: 'image4',
46
+        id: '4',
47
+        src: 'images/virtual-background/background-4.jpg'
48
+    },
49
+    {
50
+        tooltip: 'image5',
51
+        id: '5',
52
+        src: 'images/virtual-background/background-5.jpg'
53
+    },
54
+    {
55
+        tooltip: 'image6',
56
+        id: '6',
57
+        src: 'images/virtual-background/background-6.jpg'
58
+    },
59
+    {
60
+        tooltip: 'image7',
61
+        id: '7',
62
+        src: 'images/virtual-background/background-7.jpg'
63
+    }
64
+];

Loading…
取消
儲存