Parcourir la source

feat(virtual-backgrounds) add ability to upload custom images

j8
Tudor D. Pop il y a 4 ans
Parent
révision
77ee4b13e1
Aucun compte lié à l'adresse e-mail de l'auteur

+ 81
- 36
css/modals/virtual-background/_virtual-background.scss Voir le fichier

@@ -1,25 +1,64 @@
1
-.virtual-background-dialog{
2
-  display: inline-flex;
3
-  cursor: pointer;
4
-  .thumbnail{
5
-    object-fit: cover;
6
-    padding: 5px;
7
-    height: 40px;
8
-    width: 40px;
9
-  }
10
-  .thumbnail-selected{
11
-    object-fit: cover;
12
-    padding: 5px;
13
-    height: 40px;
14
-    width: 40px;
15
-    border: 2px solid #a4b8d1;
16
-  }
17
-  .blur-selected{
18
-    border: 2px solid #a4b8d1;
19
-  }
20
-  .virtual-background-none{
1
+.virtual-background-dialog {
2
+    display: inline-grid;
3
+    grid-template-columns: auto auto auto auto auto auto auto;
4
+    max-width: 370px;
5
+    cursor: pointer;
6
+    .thumbnail {
7
+        border-radius: 10px;
8
+        object-fit: cover;
9
+        padding: 5px;
10
+        height: 40px;
11
+        width: 40px;
12
+    }
13
+
14
+    .thumbnail:hover ~ .delete-image-icon {
15
+        display: block;
16
+    }
17
+    .thumbnail-selected {
18
+        border-radius: 10px;
19
+        object-fit: cover;
20
+        padding: 5px;
21
+        height: 40px;
22
+        width: 40px;
23
+        border: 2px solid #a4b8d1;
24
+    }
25
+    .blur-selected {
26
+        border-radius: 10px;
27
+        border: 2px solid #a4b8d1;
28
+    }
29
+    .virtual-background-none {
30
+        font-weight: bold;
31
+        padding: 5px;
32
+        height: 34px;
33
+        width: 34px;
34
+        border-radius: 10px;
35
+        border: 1px solid #a4b8d1;
36
+        text-align: center;
37
+        vertical-align: middle;
38
+        line-height: 35px;
39
+        margin-right: 5px;
40
+    }
41
+    .none-selected {
42
+        font-weight: bold;
43
+        padding: 5px;
44
+        height: 34px;
45
+        width: 34px;
46
+        border-radius: 10px;
47
+        border: 2px solid #a4b8d1;
48
+        text-align: center;
49
+        vertical-align: middle;
50
+        line-height: 35px;
51
+        margin-right: 5px;
52
+    }
53
+}
54
+.file-upload-btn {
55
+    display: none;
56
+}
57
+.custom-file-upload {
58
+    font-size: x-large;
21 59
     font-weight: bold;
22
-    padding: 5px;
60
+    display: inline-block;
61
+    padding: 4px;
23 62
     height: 35px;
24 63
     width: 35px;
25 64
     border-radius: 10px;
@@ -27,18 +66,24 @@
27 66
     text-align: center;
28 67
     vertical-align: middle;
29 68
     line-height: 35px;
30
-    margin-right: 5px;
31
-  }
32
-  .none-selected{
33
-    font-weight: bold;
34
-    padding: 5px;
35
-    height: 35px;
36
-    width: 35px;
37
-    border-radius: 10px;
38
-    border: 2px solid #a4b8d1;
39
-    text-align: center;
40
-    vertical-align: middle;
41
-    line-height: 35px;
42
-    margin-right: 5px;
43
-  }
44
-}
69
+    margin-left: 5px;
70
+    cursor: pointer;
71
+}
72
+
73
+.delete-image-icon {
74
+    position: absolute;
75
+    display: none;
76
+    left: 36;
77
+    bottom: 36;
78
+}
79
+.delete-image-icon:hover {
80
+    display: block;
81
+}
82
+
83
+.thumbnail-container {
84
+    position: relative;
85
+}
86
+
87
+.loading-content-text{
88
+  margin-right: 15px;
89
+}

+ 4
- 1
lang/main.json Voir le fichier

@@ -339,7 +339,10 @@
339 339
     "virtualBackground": {
340 340
         "title": "Backgrounds",
341 341
         "enableBlur": "Enable blur",
342
-        "removeBackground": "Remove background"
342
+        "removeBackground": "Remove background",
343
+        "uploadImage": "Upload image",
344
+        "pleaseWait": "Please wait...",
345
+        "none": "None"
343 346
     },
344 347
     "feedback": {
345 348
         "average": "Average",

+ 7
- 2
react/features/stream-effects/virtual-background/JitsiStreamBackgroundEffect.js Voir le fichier

@@ -5,7 +5,6 @@ import {
5 5
     SET_TIMEOUT,
6 6
     timerWorkerScript
7 7
 } from './TimerWorker';
8
-
9 8
 const blurValue = '25px';
10 9
 
11 10
 /**
@@ -114,7 +113,13 @@ export default class JitsiStreamBackgroundEffect {
114 113
 
115 114
         this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
116 115
         if (this._options.virtualBackground.isVirtualBackground) {
117
-            this._outputCanvasCtx.drawImage(this._virtualImage, 0, 0);
116
+            this._outputCanvasCtx.drawImage(
117
+                this._virtualImage,
118
+                0,
119
+                0,
120
+                this._inputVideoElement.width,
121
+                this._inputVideoElement.height
122
+            );
118 123
         } else {
119 124
             this._outputCanvasCtx.filter = `blur(${blurValue})`;
120 125
             this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);

+ 150
- 50
react/features/virtual-background/components/VirtualBackgroundDialog.js Voir le fichier

@@ -1,36 +1,37 @@
1 1
 // @flow
2 2
 /* eslint-disable react/jsx-no-bind, no-return-assign */
3
-import React, { useState } from 'react';
3
+import Spinner from '@atlaskit/spinner';
4
+import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
5
+import React, { useState, useEffect } from 'react';
6
+import uuid from 'uuid';
4 7
 
5 8
 import { Dialog } from '../../base/dialog';
6 9
 import { translate } from '../../base/i18n';
7
-import { Icon, IconBlurBackground } from '../../base/icons';
10
+import { Icon, IconBlurBackground, IconCancelSelection } from '../../base/icons';
8 11
 import { connect } from '../../base/redux';
9 12
 import { Tooltip } from '../../base/tooltip';
10 13
 import { toggleBackgroundEffect, setVirtualBackground } from '../actions';
14
+import { resizeImage, toDataURL } from '../functions';
15
+import logger from '../logger';
11 16
 
17
+// The limit of virtual background uploads is 21. When the number
18
+// of uploads is 22 we trigger the deleteStoredImage function to delete
19
+// the first/oldest uploaded background.
20
+const backgroundsLimit = 22;
12 21
 const images = [
13 22
     {
14
-        tooltip: 'Image 1',
15
-        name: 'background-1.jpg',
16 23
         id: 1,
17 24
         src: 'images/virtual-background/background-1.jpg'
18 25
     },
19 26
     {
20
-        tooltip: 'Image 2',
21
-        name: 'background-2.jpg',
22 27
         id: 2,
23 28
         src: 'images/virtual-background/background-2.jpg'
24 29
     },
25 30
     {
26
-        tooltip: 'Image 3',
27
-        name: 'background-3.jpg',
28 31
         id: 3,
29 32
         src: 'images/virtual-background/background-3.jpg'
30 33
     },
31 34
     {
32
-        tooltip: 'Image 4',
33
-        name: 'background-4.jpg',
34 35
         id: 4,
35 36
         src: 'images/virtual-background/background-4.jpg'
36 37
     }
@@ -54,23 +55,81 @@ type Props = {
54 55
  * @returns {ReactElement}
55 56
  */
56 57
 function VirtualBackground({ dispatch, t }: Props) {
58
+    const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
59
+    const [ storedImages, setStoredImages ] = useState((localImages && JSON.parse(localImages)) || []);
60
+    const [ loading, isloading ] = useState(false);
61
+
62
+    const deleteStoredImage = image => {
63
+        setStoredImages(storedImages.filter(item => item !== image));
64
+    };
65
+
66
+    /**
67
+     * Updates stored images on local storage.
68
+     */
69
+    useEffect(() => {
70
+        jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
71
+        if (storedImages.length === backgroundsLimit) {
72
+            deleteStoredImage(storedImages[0]);
73
+        }
74
+    }, [ storedImages ]);
75
+
57 76
     const [ selected, setSelected ] = useState('');
58
-    const enableBlur = () => {
77
+    const enableBlur = async () => {
78
+        isloading(true);
59 79
         setSelected('blur');
60
-        dispatch(setVirtualBackground('', false));
61
-        dispatch(toggleBackgroundEffect(true));
80
+        await dispatch(setVirtualBackground('', false));
81
+        await dispatch(toggleBackgroundEffect(true));
82
+        isloading(false);
62 83
     };
63 84
 
64
-    const removeBackground = () => {
85
+    const removeBackground = async () => {
86
+        isloading(true);
65 87
         setSelected('none');
66
-        dispatch(setVirtualBackground('', false));
67
-        dispatch(toggleBackgroundEffect(false));
88
+        await dispatch(setVirtualBackground('', false));
89
+        await dispatch(toggleBackgroundEffect(false));
90
+        isloading(false);
91
+    };
92
+
93
+    const setUploadedImageBackground = async image => {
94
+        isloading(true);
95
+        setSelected(image.id);
96
+        await dispatch(setVirtualBackground(image.src, true));
97
+        await dispatch(toggleBackgroundEffect(true));
98
+        isloading(false);
68 99
     };
69 100
 
70
-    const addImageBackground = image => {
101
+    const setImageBackground = async image => {
102
+        isloading(true);
71 103
         setSelected(image.id);
72
-        dispatch(setVirtualBackground(image.src, true));
73
-        dispatch(toggleBackgroundEffect(true));
104
+        await dispatch(setVirtualBackground(await toDataURL(image.src), true));
105
+        await dispatch(toggleBackgroundEffect(true));
106
+        isloading(false);
107
+    };
108
+
109
+    const uploadImage = async imageFile => {
110
+        const reader = new FileReader();
111
+
112
+        reader.readAsDataURL(imageFile[0]);
113
+        reader.onload = async () => {
114
+            const resizedImage = await resizeImage(reader.result);
115
+
116
+            isloading(true);
117
+            setStoredImages([
118
+                ...storedImages,
119
+                {
120
+                    id: uuid.v4(),
121
+                    src: resizedImage
122
+                }
123
+            ]);
124
+
125
+            await dispatch(setVirtualBackground(resizedImage, true));
126
+            await dispatch(toggleBackgroundEffect(true));
127
+            isloading(false);
128
+        };
129
+        reader.onerror = () => {
130
+            isloading(false);
131
+            logger.error('Failed to upload virtual image!');
132
+        };
74 133
     };
75 134
 
76 135
     return (
@@ -79,38 +138,79 @@ function VirtualBackground({ dispatch, t }: Props) {
79 138
             submitDisabled = { false }
80 139
             titleKey = { 'virtualBackground.title' }
81 140
             width = 'small'>
82
-            <div className = 'virtual-background-dialog'>
83
-                <Tooltip
84
-                    content = { t('virtualBackground.removeBackground') }
85
-                    position = { 'top' }>
86
-                    <div
87
-                        className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
88
-                        onClick = { () => removeBackground() }>
89
-                        None
141
+            {loading ? (
142
+                <div>
143
+                    <span className = 'loading-content-text'>{t('virtualBackground.pleaseWait')}</span>
144
+                    <Spinner
145
+                        isCompleting = { false }
146
+                        size = 'medium' />
147
+                </div>
148
+            ) : (
149
+                <div>
150
+                    <div className = 'virtual-background-dialog'>
151
+                        <Tooltip
152
+                            content = { t('virtualBackground.removeBackground') }
153
+                            position = { 'top' }>
154
+                            <div
155
+                                className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
156
+                                onClick = { removeBackground }>
157
+                                {t('virtualBackground.none')}
158
+                            </div>
159
+                        </Tooltip>
160
+                        <Tooltip
161
+                            content = { t('virtualBackground.enableBlur') }
162
+                            position = { 'top' }>
163
+                            <Icon
164
+                                className = { selected === 'blur' ? 'blur-selected' : '' }
165
+                                onClick = { () => enableBlur() }
166
+                                size = { 50 }
167
+                                src = { IconBlurBackground } />
168
+                        </Tooltip>
169
+                        {images.map((image, index) => (
170
+                            <img
171
+                                className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
172
+                                key = { index }
173
+                                onClick = { () => setImageBackground(image) }
174
+                                onError = { event => event.target.style.display = 'none' }
175
+                                src = { image.src } />
176
+                        ))}
177
+                        <Tooltip
178
+                            content = { t('virtualBackground.uploadImage') }
179
+                            position = { 'top' }>
180
+                            <label
181
+                                className = 'custom-file-upload'
182
+                                htmlFor = 'file-upload'>
183
+                                +
184
+                            </label>
185
+                            <input
186
+                                accept = 'image/*'
187
+                                className = 'file-upload-btn'
188
+                                id = 'file-upload'
189
+                                onChange = { e => uploadImage(e.target.files) }
190
+                                type = 'file' />
191
+                        </Tooltip>
192
+                    </div>
193
+
194
+                    <div className = 'virtual-background-dialog'>
195
+                        {storedImages.map((image, index) => (
196
+                            <div
197
+                                className = { 'thumbnail-container' }
198
+                                key = { index }>
199
+                                <img
200
+                                    className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
201
+                                    onClick = { () => setUploadedImageBackground(image) }
202
+                                    onError = { event => event.target.style.display = 'none' }
203
+                                    src = { image.src } />
204
+                                <Icon
205
+                                    className = { 'delete-image-icon' }
206
+                                    onClick = { () => deleteStoredImage(image) }
207
+                                    size = { 15 }
208
+                                    src = { IconCancelSelection } />
209
+                            </div>
210
+                        ))}
90 211
                     </div>
91
-                </Tooltip>
92
-                <Tooltip
93
-                    content = { t('virtualBackground.enableBlur') }
94
-                    position = { 'top' }>
95
-                    <Icon
96
-                        className = { selected === 'blur' ? 'blur-selected' : '' }
97
-                        onClick = { () => enableBlur() }
98
-                        size = { 50 }
99
-                        src = { IconBlurBackground } />
100
-                </Tooltip>
101
-                {images.map((image, index) => (
102
-                    <Tooltip
103
-                        content = { image.tooltip }
104
-                        key = { index }
105
-                        position = { 'top' }>
106
-                        <img
107
-                            className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
108
-                            onClick = { () => addImageBackground(image) }
109
-                            onError = { event => event.target.style.display = 'none' }
110
-                            src = { image.src } />
111
-                    </Tooltip>
112
-                ))}
113
-            </div>
212
+                </div>
213
+            )}
114 214
         </Dialog>
115 215
     );
116 216
 }

+ 59
- 2
react/features/virtual-background/functions.js Voir le fichier

@@ -1,6 +1,4 @@
1 1
 // @flow
2
-
3
-
4 2
 let filterSupport;
5 3
 
6 4
 /**
@@ -20,3 +18,62 @@ export function checkBlurSupport() {
20 18
 
21 19
     return filterSupport;
22 20
 }
21
+
22
+/**
23
+ * Convert blob to base64.
24
+ *
25
+ * @param {Blob} blob - The link to add info with.
26
+ * @returns {Promise<string>}
27
+ */
28
+export const blobToData = (blob: Blob): Promise<string> => new Promise(resolve => {
29
+    const reader = new FileReader();
30
+
31
+    reader.onloadend = () => resolve(reader.result.toString());
32
+    reader.readAsDataURL(blob);
33
+});
34
+
35
+/**
36
+ * Convert blob to base64.
37
+ *
38
+ * @param {string} url - The image url.
39
+ * @returns {Object} - Returns the converted blob to base64.
40
+ */
41
+export const toDataURL = async (url: string) => {
42
+    const response = await fetch(url);
43
+    const blob = await response.blob();
44
+    const resData = await blobToData(blob);
45
+
46
+    return resData;
47
+};
48
+
49
+/**
50
+ * Resize image and adjust original aspect ratio.
51
+ *
52
+ * @param {Object} base64image - Base64 image extraction.
53
+ * @param {number} width - Value for resizing the image width.
54
+ * @param {number} height - Value for resizing the image height.
55
+ * @returns {Object} Returns the canvas output.
56
+ *
57
+ */
58
+export async function resizeImage(base64image: any, width: number = 1920, height: number = 1080) {
59
+    const img = document.createElement('img');
60
+
61
+    img.src = base64image;
62
+    /* eslint-disable no-empty-function */
63
+    img.onload = await function() {};
64
+
65
+    // Create an off-screen canvas.
66
+    const canvas = document.createElement('canvas');
67
+    const ctx = canvas.getContext('2d');
68
+
69
+    // Set its dimension to target size.
70
+    canvas.width = width;
71
+    canvas.height = height;
72
+
73
+    // Draw source image into the off-screen canvas.
74
+    // TODO: keep aspect ratio and implement object-fit: cover.
75
+    ctx.drawImage(img, 0, 0, width, height);
76
+
77
+    // Encode image to data-uri with base64 version of compressed image.
78
+    return canvas.toDataURL('image/jpeg', 0.5);
79
+}

Chargement…
Annuler
Enregistrer