feat(virtual-backgrounds) add ability to upload custom images
This commit is contained in:
parent
a3a2ce3875
commit
77ee4b13e1
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue