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: '', // 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. // Sets the background transparency level. '0' is fully transparent, '1' is opaque.
// backgroundAlpha: 1, // backgroundAlpha: 1,

View File

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

View File

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

View File

@ -7,8 +7,8 @@
* @param {string} path - The URL path. * @param {string} path - The URL path.
* @returns {string} * @returns {string}
*/ */
export function extractFqnFromPath(path: string) { export function extractFqnFromPath() {
const parts = path.split('/'); const parts = window.location.pathname.split('/');
const len = parts.length; const len = parts.length;
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : ''; 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 baseUrl = state['features/base/config'].brandingDataUrl;
const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname); const fqn = extractFqnFromPath();
if (baseUrl && fqn) { if (baseUrl && fqn) {
return `${baseUrl}?conferenceFqn=${encodeURIComponent(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 // @flow
import { ReducerRegistry } from '../base/redux'; import { ReducerRegistry } from '../base/redux';
import { type Image } from '../virtual-background/constants';
import { import {
SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_DATA,
@ -113,7 +114,15 @@ const DEFAULT_STATE = {
* @public * @public
* @type {boolean} * @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, inviteDomain,
logoClickUrl, logoClickUrl,
logoImageUrl, logoImageUrl,
premeetingBackground premeetingBackground,
virtualBackgrounds
} = action.value; } = action.value;
return { return {
@ -146,7 +156,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
premeetingBackground, premeetingBackground,
customizationFailed: false, customizationFailed: false,
customizationReady: true, customizationReady: true,
useDynamicBrandingData: true useDynamicBrandingData: true,
virtualBackgrounds: formatImages(virtualBackgrounds || [])
}; };
} }
case SET_DYNAMIC_BRANDING_FAILED: { case SET_DYNAMIC_BRANDING_FAILED: {
@ -166,3 +177,30 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
return state; 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(); return Promise.resolve();
} }
const meetingFqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname); const meetingFqn = extractFqnFromPath();
const feedbackData = { const feedbackData = {
...feedback, ...feedback,
sessionId: conference.sessionId, sessionId: conference.sessionId,

View File

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

View File

@ -55,7 +55,6 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
const { webhookProxyUrl: url } = state['features/base/config']; const { webhookProxyUrl: url } = state['features/base/config'];
const { conference } = state['features/base/conference']; const { conference } = state['features/base/conference'];
const { jwt } = state['features/base/jwt']; const { jwt } = state['features/base/jwt'];
const { locationURL } = state['features/base/connection'];
const localParticipant = getLocalParticipant(state); const localParticipant = getLocalParticipant(state);
const headers = { const headers = {
@ -65,7 +64,7 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
const reqBody = { const reqBody = {
meetingFqn: extractFqnFromPath(locationURL.pathname), meetingFqn: extractFqnFromPath(),
sessionId: conference.sessionId, sessionId: conference.sessionId,
submitted: Date.now(), submitted: Date.now(),
reactions, 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 Spinner from '@atlaskit/spinner';
import Bourne from '@hapi/bourne'; import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage'; import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import uuid from 'uuid';
import { Dialog, hideDialog, openDialog } from '../../base/dialog'; import { Dialog, hideDialog, openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n'; 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 { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions'; import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
import { VIDEO_TYPE } from '../../base/media'; import { VIDEO_TYPE } from '../../base/media';
@ -18,62 +17,20 @@ import { Tooltip } from '../../base/tooltip';
import { getLocalVideoTrack } from '../../base/tracks'; import { getLocalVideoTrack } from '../../base/tracks';
import { showErrorNotification } from '../../notifications'; import { showErrorNotification } from '../../notifications';
import { toggleBackgroundEffect } from '../actions'; import { toggleBackgroundEffect } from '../actions';
import { VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
import { resizeImage, toDataURL } from '../functions'; import { toDataURL } from '../functions';
import logger from '../logger'; import logger from '../logger';
import UploadImageButton from './UploadImageButton';
import VirtualBackgroundPreview from './VirtualBackgroundPreview'; 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 = { type Props = {
/**
* The list of Images to choose from.
*/
_images: Array<Image>,
/** /**
* The current local flip x status. * The current local flip x status.
*/ */
@ -89,6 +46,11 @@ type Props = {
*/ */
_selectedThumbnail: string, _selectedThumbnail: string,
/**
* If the upload button should be displayed or not.
*/
_showUploadButton: boolean,
/** /**
* Returns the selected virtual background object. * Returns the selected virtual background object.
*/ */
@ -128,11 +90,15 @@ const onError = event => {
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { localFlipX } = state['features/base/settings']; const { localFlipX } = state['features/base/settings'];
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
return { return {
_localFlipX: Boolean(localFlipX), _localFlipX: Boolean(localFlipX),
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
_virtualBackground: state['features/virtual-background'], _virtualBackground: state['features/virtual-background'],
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail, _selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
}; };
} }
@ -145,9 +111,11 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function VirtualBackground({ function VirtualBackground({
_localFlipX, _images,
_jitsiTrack, _jitsiTrack,
_localFlipX,
_selectedThumbnail, _selectedThumbnail,
_showUploadButton,
_virtualBackground, _virtualBackground,
dispatch, dispatch,
initialOptions, initialOptions,
@ -158,7 +126,7 @@ function VirtualBackground({
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds'); const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []); const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const uploadImageButton: Object = useRef(null);
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
? _virtualBackground.virtualSource ? _virtualBackground.virtualSource
: null); : null);
@ -186,7 +154,7 @@ function VirtualBackground({
// Preventing localStorage QUOTA_EXCEEDED_ERR // Preventing localStorage QUOTA_EXCEEDED_ERR
err && setStoredImages(storedImages.slice(1)); err && setStoredImages(storedImages.slice(1));
} }
if (storedImages.length === backgroundsLimit) { if (storedImages.length === BACKGROUNDS_LIMIT) {
setStoredImages(storedImages.slice(1)); setStoredImages(storedImages.slice(1));
} }
}, [ storedImages ]); }, [ storedImages ]);
@ -321,61 +289,27 @@ function VirtualBackground({
const setImageBackground = useCallback(async e => { const setImageBackground = useCallback(async e => {
const imageId = e.currentTarget.getAttribute('data-imageid'); 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) { if (image) {
const url = await toDataURL(image.src); try {
const url = await toDataURL(image.src);
setOptions({ setOptions({
backgroundType: 'image', backgroundType: 'image',
enabled: true, enabled: true,
url, url,
selectedThumbnail: image.id selectedThumbnail: image.id
}); });
logger.info('Image setted for virtual background preview!'); logger.info('Image set for virtual background preview!');
} catch (err) {
logger.error('Could not fetch virtual background image:', err);
}
setLoading(false); 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 => { const setImageBackgroundKeyPress = useCallback(e => {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
@ -448,25 +382,13 @@ function VirtualBackground({
</div> </div>
) : ( ) : (
<div> <div>
{previewIsLoaded && <label {_showUploadButton
aria-label = { t('virtualBackground.uploadImage') } && <UploadImageButton
className = 'file-upload-label' setLoading = { setLoading }
htmlFor = 'file-upload' setOptions = { setOptions }
onKeyPress = { uploadImageKeyPress } setStoredImages = { setStoredImages }
tabIndex = { 0 } > showLabel = { previewIsLoaded }
<Icon storedImages = { storedImages } />}
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' />
<div <div
className = 'virtual-background-dialog' className = 'virtual-background-dialog'
role = 'radiogroup' role = 'radiogroup'
@ -535,7 +457,7 @@ function VirtualBackground({
src = { IconShareDesktop } /> src = { IconShareDesktop } />
</div> </div>
</Tooltip> </Tooltip>
{images.map(image => ( {_images.map(image => (
<Tooltip <Tooltip
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) } content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
key = { image.id } key = { image.id }

View File

@ -1,3 +1,5 @@
// @flow
/** /**
* An enumeration of the different virtual background types. * An enumeration of the different virtual background types.
* *
@ -9,3 +11,54 @@ export const VIRTUAL_BACKGROUND_TYPE = {
BLUR: 'blur', BLUR: 'blur',
NONE: 'none' 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'
}
];