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

This commit is contained in:
Vlad Piersec 2021-09-10 10:00:54 +03:00 committed by vp8x8
parent 57083c174f
commit f9cc813e91
12 changed files with 294 additions and 151 deletions

View File

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

View File

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

View File

@ -84,6 +84,7 @@ export default [
'disableAEC',
'disableAGC',
'disableAP',
'disableAddingBackgroundImages',
'disableAudioLevels',
'disableChatSmileys',
'disableDeepLinking',

View File

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

View File

@ -0,0 +1,18 @@
// @flow
import { APP_WILL_MOUNT } from '../base/app';
import { MiddlewareRegistry } from '../base/redux';
import { fetchCustomBrandingData } from './actions';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT: {
store.dispatch(fetchCustomBrandingData());
break;
}
}
return next(action);
});

View File

@ -1,6 +1,7 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { type Image } from '../virtual-background/constants';
import {
SET_DYNAMIC_BRANDING_DATA,
@ -113,7 +114,15 @@ const DEFAULT_STATE = {
* @public
* @type {boolean}
*/
useDynamicBrandingData: false
useDynamicBrandingData: false,
/**
* An array of images to be used as virtual backgrounds instead of the default ones.
*
* @public
* @type {Array<Object>}
*/
virtualBackgrounds: []
};
/**
@ -131,7 +140,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
premeetingBackground
premeetingBackground,
virtualBackgrounds
} = action.value;
return {
@ -146,7 +156,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
premeetingBackground,
customizationFailed: false,
customizationReady: true,
useDynamicBrandingData: true
useDynamicBrandingData: true,
virtualBackgrounds: formatImages(virtualBackgrounds || [])
};
}
case SET_DYNAMIC_BRANDING_FAILED: {
@ -166,3 +177,30 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
return state;
});
/**
* Transforms the branding images into an array of Images objects ready
* to be used as virtual backgrounds.
*
* @param {Array<string>} images -
* @private
* @returns {{Props}}
*/
function formatImages(images: Array<string> | Array<Object>): Array<Image> {
return images.map((img, i) => {
let src;
let tooltip;
if (typeof img === 'object') {
({ src, tooltip } = img);
} else {
src = img;
}
return {
id: `branding-${i}`,
src,
tooltip
};
});
}

View File

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

View File

@ -5,7 +5,6 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { fetchCustomBrandingData } from '../../dynamic-branding';
import { SharedVideo } from '../../shared-video/components/web';
import { Captions } from '../../subtitles/';
@ -28,11 +27,6 @@ type Props = {
*/
_customBackgroundImageUrl: string,
/**
* Fetches the branding data.
*/
_fetchCustomBrandingData: Function,
/**
* Prop that indicates whether the chat is open.
*/
@ -52,14 +46,6 @@ type Props = {
* @extends Component
*/
class LargeVideo extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this.props._fetchCustomBrandingData();
}
/**
* Implements React's {@link Component#render()}.
@ -167,8 +153,4 @@ function _mapStateToProps(state) {
};
}
const _mapDispatchToProps = {
_fetchCustomBrandingData: fetchCustomBrandingData
};
export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);
export default connect(_mapStateToProps)(LargeVideo);

View File

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

View File

@ -0,0 +1,125 @@
// @flow
import React, { useCallback, useRef } from 'react';
import uuid from 'uuid';
import { translate } from '../../base/i18n';
import { Icon, IconPlusCircle } from '../../base/icons';
import { VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
import { resizeImage } from '../functions';
import logger from '../logger';
type Props = {
/**
* Callback used to set the 'loading' state of the parent component.
*/
setLoading: Function,
/**
* Callback used to set the options.
*/
setOptions: Function,
/**
* Callback used to set the storedImages array.
*/
setStoredImages: Function,
/**
* A list of images locally stored.
*/
storedImages: Array<Image>,
/**
* If a label should be displayed alongside the button.
*/
showLabel: boolean,
/**
* Used for translation.
*/
t: Function
}
/**
* Component used to upload an image.
*
* @param {Object} Props - The props of the component.
* @returns {React$Node}
*/
function UploadImageButton({
setLoading,
setOptions,
setStoredImages,
showLabel,
storedImages,
t
}: Props) {
const uploadImageButton: Object = useRef(null);
const uploadImageKeyPress = useCallback(e => {
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
uploadImageButton.current.click();
}
}, [ uploadImageButton.current ]);
const uploadImage = useCallback(async e => {
const reader = new FileReader();
const imageFile = e.target.files;
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const url = await resizeImage(reader.result);
const uuId = uuid.v4();
setStoredImages([
...storedImages,
{
id: uuId,
src: url
}
]);
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
enabled: true,
url,
selectedThumbnail: uuId
});
};
logger.info('New virtual background image uploaded!');
reader.onerror = () => {
setLoading(false);
logger.error('Failed to upload virtual image!');
};
}, [ storedImages ]);
return (
<>
{showLabel && <label
aria-label = { t('virtualBackground.uploadImage') }
className = 'file-upload-label'
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }
tabIndex = { 0 } >
<Icon
className = { 'add-background' }
size = { 20 }
src = { IconPlusCircle } />
{t('virtualBackground.addBackground')}
</label>}
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }
type = 'file' />
</>
);
}
export default translate(UploadImageButton);

View File

@ -3,12 +3,11 @@
import Spinner from '@atlaskit/spinner';
import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import uuid from 'uuid';
import React, { useState, useEffect, useCallback } from 'react';
import { Dialog, hideDialog, openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { Icon, IconCloseSmall, IconPlusCircle, IconShareDesktop } from '../../base/icons';
import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
import { VIDEO_TYPE } from '../../base/media';
@ -18,62 +17,20 @@ import { Tooltip } from '../../base/tooltip';
import { getLocalVideoTrack } from '../../base/tracks';
import { showErrorNotification } from '../../notifications';
import { toggleBackgroundEffect } from '../actions';
import { VIRTUAL_BACKGROUND_TYPE } from '../constants';
import { resizeImage, toDataURL } from '../functions';
import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
import { toDataURL } from '../functions';
import logger from '../logger';
import UploadImageButton from './UploadImageButton';
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
type Image = {
tooltip?: string,
id: string,
src: string
}
// The limit of virtual background uploads is 24. When the number
// of uploads is 25 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
const backgroundsLimit = 25;
const images: Array<Image> = [
{
tooltip: 'image1',
id: '1',
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'image2',
id: '2',
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'image3',
id: '3',
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'image4',
id: '4',
src: 'images/virtual-background/background-4.jpg'
},
{
tooltip: 'image5',
id: '5',
src: 'images/virtual-background/background-5.jpg'
},
{
tooltip: 'image6',
id: '6',
src: 'images/virtual-background/background-6.jpg'
},
{
tooltip: 'image7',
id: '7',
src: 'images/virtual-background/background-7.jpg'
}
];
type Props = {
/**
* The list of Images to choose from.
*/
_images: Array<Image>,
/**
* The current local flip x status.
*/
@ -89,6 +46,11 @@ type Props = {
*/
_selectedThumbnail: string,
/**
* If the upload button should be displayed or not.
*/
_showUploadButton: boolean,
/**
* Returns the selected virtual background object.
*/
@ -128,11 +90,15 @@ const onError = event => {
*/
function _mapStateToProps(state): Object {
const { localFlipX } = state['features/base/settings'];
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
return {
_localFlipX: Boolean(localFlipX),
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
_virtualBackground: state['features/virtual-background'],
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
};
}
@ -145,9 +111,11 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg
* @returns {ReactElement}
*/
function VirtualBackground({
_localFlipX,
_images,
_jitsiTrack,
_localFlipX,
_selectedThumbnail,
_showUploadButton,
_virtualBackground,
dispatch,
initialOptions,
@ -158,7 +126,7 @@ function VirtualBackground({
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false);
const uploadImageButton: Object = useRef(null);
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
? _virtualBackground.virtualSource
: null);
@ -186,7 +154,7 @@ function VirtualBackground({
// Preventing localStorage QUOTA_EXCEEDED_ERR
err && setStoredImages(storedImages.slice(1));
}
if (storedImages.length === backgroundsLimit) {
if (storedImages.length === BACKGROUNDS_LIMIT) {
setStoredImages(storedImages.slice(1));
}
}, [ storedImages ]);
@ -321,61 +289,27 @@ function VirtualBackground({
const setImageBackground = useCallback(async e => {
const imageId = e.currentTarget.getAttribute('data-imageid');
const image = images.find(img => img.id === imageId);
const image = _images.find(img => img.id === imageId);
if (image) {
const url = await toDataURL(image.src);
try {
const url = await toDataURL(image.src);
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
});
logger.info('Image setted for virtual background preview!');
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
});
logger.info('Image set for virtual background preview!');
} catch (err) {
logger.error('Could not fetch virtual background image:', err);
}
setLoading(false);
}
}, []);
const uploadImage = useCallback(async e => {
const reader = new FileReader();
const imageFile = e.target.files;
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const url = await resizeImage(reader.result);
const uuId = uuid.v4();
setStoredImages([
...storedImages,
{
id: uuId,
src: url
}
]);
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
enabled: true,
url,
selectedThumbnail: uuId
});
};
logger.info('New virtual background image uploaded!');
reader.onerror = () => {
setLoading(false);
logger.error('Failed to upload virtual image!');
};
}, [ dispatch, storedImages ]);
const uploadImageKeyPress = useCallback(e => {
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
uploadImageButton.current.click();
}
}, [ uploadImageButton.current ]);
const setImageBackgroundKeyPress = useCallback(e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
@ -448,25 +382,13 @@ function VirtualBackground({
</div>
) : (
<div>
{previewIsLoaded && <label
aria-label = { t('virtualBackground.uploadImage') }
className = 'file-upload-label'
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }
tabIndex = { 0 } >
<Icon
className = { 'add-background' }
size = { 20 }
src = { IconPlusCircle } />
{t('virtualBackground.addBackground')}
</label> }
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }
type = 'file' />
{_showUploadButton
&& <UploadImageButton
setLoading = { setLoading }
setOptions = { setOptions }
setStoredImages = { setStoredImages }
showLabel = { previewIsLoaded }
storedImages = { storedImages } />}
<div
className = 'virtual-background-dialog'
role = 'radiogroup'
@ -535,7 +457,7 @@ function VirtualBackground({
src = { IconShareDesktop } />
</div>
</Tooltip>
{images.map(image => (
{_images.map(image => (
<Tooltip
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
key = { image.id }

View File

@ -1,3 +1,5 @@
// @flow
/**
* An enumeration of the different virtual background types.
*
@ -9,3 +11,54 @@ export const VIRTUAL_BACKGROUND_TYPE = {
BLUR: 'blur',
NONE: 'none'
};
export type Image = {
tooltip?: string,
id: string,
src: string
}
// The limit of virtual background uploads is 24. When the number
// of uploads is 25 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
export const BACKGROUNDS_LIMIT = 25;
export const IMAGES: Array<Image> = [
{
tooltip: 'image1',
id: '1',
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'image2',
id: '2',
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'image3',
id: '3',
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'image4',
id: '4',
src: 'images/virtual-background/background-4.jpg'
},
{
tooltip: 'image5',
id: '5',
src: 'images/virtual-background/background-5.jpg'
},
{
tooltip: 'image6',
id: '6',
src: 'images/virtual-background/background-6.jpg'
},
{
tooltip: 'image7',
id: '7',
src: 'images/virtual-background/background-7.jpg'
}
];