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

This commit is contained in:
Tudor D. Pop 2021-03-24 18:32:45 +02:00 committed by GitHub
parent a3a2ce3875
commit 77ee4b13e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 301 additions and 91 deletions

View File

@ -1,25 +1,64 @@
.virtual-background-dialog{
display: inline-flex;
cursor: pointer;
.thumbnail{
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
}
.thumbnail-selected{
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
border: 2px solid #a4b8d1;
}
.blur-selected{
border: 2px solid #a4b8d1;
}
.virtual-background-none{
.virtual-background-dialog {
display: inline-grid;
grid-template-columns: auto auto auto auto auto auto auto;
max-width: 370px;
cursor: pointer;
.thumbnail {
border-radius: 10px;
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
}
.thumbnail:hover ~ .delete-image-icon {
display: block;
}
.thumbnail-selected {
border-radius: 10px;
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
border: 2px solid #a4b8d1;
}
.blur-selected {
border-radius: 10px;
border: 2px solid #a4b8d1;
}
.virtual-background-none {
font-weight: bold;
padding: 5px;
height: 34px;
width: 34px;
border-radius: 10px;
border: 1px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
.none-selected {
font-weight: bold;
padding: 5px;
height: 34px;
width: 34px;
border-radius: 10px;
border: 2px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
}
.file-upload-btn {
display: none;
}
.custom-file-upload {
font-size: x-large;
font-weight: bold;
padding: 5px;
display: inline-block;
padding: 4px;
height: 35px;
width: 35px;
border-radius: 10px;
@ -27,18 +66,24 @@
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
.none-selected{
font-weight: bold;
padding: 5px;
height: 35px;
width: 35px;
border-radius: 10px;
border: 2px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
}
margin-left: 5px;
cursor: pointer;
}
.delete-image-icon {
position: absolute;
display: none;
left: 36;
bottom: 36;
}
.delete-image-icon:hover {
display: block;
}
.thumbnail-container {
position: relative;
}
.loading-content-text{
margin-right: 15px;
}

View File

@ -339,7 +339,10 @@
"virtualBackground": {
"title": "Backgrounds",
"enableBlur": "Enable blur",
"removeBackground": "Remove background"
"removeBackground": "Remove background",
"uploadImage": "Upload image",
"pleaseWait": "Please wait...",
"none": "None"
},
"feedback": {
"average": "Average",

View File

@ -5,7 +5,6 @@ import {
SET_TIMEOUT,
timerWorkerScript
} from './TimerWorker';
const blurValue = '25px';
/**
@ -114,7 +113,13 @@ export default class JitsiStreamBackgroundEffect {
this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
if (this._options.virtualBackground.isVirtualBackground) {
this._outputCanvasCtx.drawImage(this._virtualImage, 0, 0);
this._outputCanvasCtx.drawImage(
this._virtualImage,
0,
0,
this._inputVideoElement.width,
this._inputVideoElement.height
);
} else {
this._outputCanvasCtx.filter = `blur(${blurValue})`;
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);

View File

@ -1,36 +1,37 @@
// @flow
/* eslint-disable react/jsx-no-bind, no-return-assign */
import React, { useState } from 'react';
import Spinner from '@atlaskit/spinner';
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 { translate } from '../../base/i18n';
import { Icon, IconBlurBackground } from '../../base/icons';
import { Icon, IconBlurBackground, IconCancelSelection } from '../../base/icons';
import { connect } from '../../base/redux';
import { Tooltip } from '../../base/tooltip';
import { toggleBackgroundEffect, setVirtualBackground } from '../actions';
import { resizeImage, toDataURL } from '../functions';
import logger from '../logger';
// The limit of virtual background uploads is 21. When the number
// of uploads is 22 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
const backgroundsLimit = 22;
const images = [
{
tooltip: 'Image 1',
name: 'background-1.jpg',
id: 1,
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'Image 2',
name: 'background-2.jpg',
id: 2,
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'Image 3',
name: 'background-3.jpg',
id: 3,
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'Image 4',
name: 'background-4.jpg',
id: 4,
src: 'images/virtual-background/background-4.jpg'
}
@ -54,23 +55,81 @@ type Props = {
* @returns {ReactElement}
*/
function VirtualBackground({ dispatch, t }: Props) {
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState((localImages && JSON.parse(localImages)) || []);
const [ loading, isloading ] = useState(false);
const deleteStoredImage = image => {
setStoredImages(storedImages.filter(item => item !== image));
};
/**
* Updates stored images on local storage.
*/
useEffect(() => {
jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
if (storedImages.length === backgroundsLimit) {
deleteStoredImage(storedImages[0]);
}
}, [ storedImages ]);
const [ selected, setSelected ] = useState('');
const enableBlur = () => {
const enableBlur = async () => {
isloading(true);
setSelected('blur');
dispatch(setVirtualBackground('', false));
dispatch(toggleBackgroundEffect(true));
await dispatch(setVirtualBackground('', false));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const removeBackground = () => {
const removeBackground = async () => {
isloading(true);
setSelected('none');
dispatch(setVirtualBackground('', false));
dispatch(toggleBackgroundEffect(false));
await dispatch(setVirtualBackground('', false));
await dispatch(toggleBackgroundEffect(false));
isloading(false);
};
const addImageBackground = image => {
const setUploadedImageBackground = async image => {
isloading(true);
setSelected(image.id);
dispatch(setVirtualBackground(image.src, true));
dispatch(toggleBackgroundEffect(true));
await dispatch(setVirtualBackground(image.src, true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const setImageBackground = async image => {
isloading(true);
setSelected(image.id);
await dispatch(setVirtualBackground(await toDataURL(image.src), true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const uploadImage = async imageFile => {
const reader = new FileReader();
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const resizedImage = await resizeImage(reader.result);
isloading(true);
setStoredImages([
...storedImages,
{
id: uuid.v4(),
src: resizedImage
}
]);
await dispatch(setVirtualBackground(resizedImage, true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
reader.onerror = () => {
isloading(false);
logger.error('Failed to upload virtual image!');
};
};
return (
@ -79,38 +138,79 @@ function VirtualBackground({ dispatch, t }: Props) {
submitDisabled = { false }
titleKey = { 'virtualBackground.title' }
width = 'small'>
<div className = 'virtual-background-dialog'>
<Tooltip
content = { t('virtualBackground.removeBackground') }
position = { 'top' }>
<div
className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
onClick = { () => removeBackground() }>
None
{loading ? (
<div>
<span className = 'loading-content-text'>{t('virtualBackground.pleaseWait')}</span>
<Spinner
isCompleting = { false }
size = 'medium' />
</div>
) : (
<div>
<div className = 'virtual-background-dialog'>
<Tooltip
content = { t('virtualBackground.removeBackground') }
position = { 'top' }>
<div
className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
onClick = { removeBackground }>
{t('virtualBackground.none')}
</div>
</Tooltip>
<Tooltip
content = { t('virtualBackground.enableBlur') }
position = { 'top' }>
<Icon
className = { selected === 'blur' ? 'blur-selected' : '' }
onClick = { () => enableBlur() }
size = { 50 }
src = { IconBlurBackground } />
</Tooltip>
{images.map((image, index) => (
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
key = { index }
onClick = { () => setImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
))}
<Tooltip
content = { t('virtualBackground.uploadImage') }
position = { 'top' }>
<label
className = 'custom-file-upload'
htmlFor = 'file-upload'>
+
</label>
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { e => uploadImage(e.target.files) }
type = 'file' />
</Tooltip>
</div>
</Tooltip>
<Tooltip
content = { t('virtualBackground.enableBlur') }
position = { 'top' }>
<Icon
className = { selected === 'blur' ? 'blur-selected' : '' }
onClick = { () => enableBlur() }
size = { 50 }
src = { IconBlurBackground } />
</Tooltip>
{images.map((image, index) => (
<Tooltip
content = { image.tooltip }
key = { index }
position = { 'top' }>
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
onClick = { () => addImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
</Tooltip>
))}
</div>
<div className = 'virtual-background-dialog'>
{storedImages.map((image, index) => (
<div
className = { 'thumbnail-container' }
key = { index }>
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
onClick = { () => setUploadedImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
<Icon
className = { 'delete-image-icon' }
onClick = { () => deleteStoredImage(image) }
size = { 15 }
src = { IconCancelSelection } />
</div>
))}
</div>
</div>
)}
</Dialog>
);
}

View File

@ -1,6 +1,4 @@
// @flow
let filterSupport;
/**
@ -20,3 +18,62 @@ export function checkBlurSupport() {
return filterSupport;
}
/**
* Convert blob to base64.
*
* @param {Blob} blob - The link to add info with.
* @returns {Promise<string>}
*/
export const blobToData = (blob: Blob): Promise<string> => new Promise(resolve => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.toString());
reader.readAsDataURL(blob);
});
/**
* Convert blob to base64.
*
* @param {string} url - The image url.
* @returns {Object} - Returns the converted blob to base64.
*/
export const toDataURL = async (url: string) => {
const response = await fetch(url);
const blob = await response.blob();
const resData = await blobToData(blob);
return resData;
};
/**
* Resize image and adjust original aspect ratio.
*
* @param {Object} base64image - Base64 image extraction.
* @param {number} width - Value for resizing the image width.
* @param {number} height - Value for resizing the image height.
* @returns {Object} Returns the canvas output.
*
*/
export async function resizeImage(base64image: any, width: number = 1920, height: number = 1080) {
const img = document.createElement('img');
img.src = base64image;
/* eslint-disable no-empty-function */
img.onload = await function() {};
// Create an off-screen canvas.
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set its dimension to target size.
canvas.width = width;
canvas.height = height;
// Draw source image into the off-screen canvas.
// TODO: keep aspect ratio and implement object-fit: cover.
ctx.drawImage(img, 0, 0, width, height);
// Encode image to data-uri with base64 version of compressed image.
return canvas.toDataURL('image/jpeg', 0.5);
}