Merge pull request #14794 from JoVictorNunes/virtual-background-upload
feat(video): upload of own webcam virtual background
This commit is contained in:
commit
ca762747a3
@ -162,6 +162,7 @@ export default class ButtonBase extends React.Component {
|
||||
'small',
|
||||
'full',
|
||||
'iconRight',
|
||||
'isVisualEffects',
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -75,7 +75,7 @@ class ConfirmationModal extends Component {
|
||||
{description}
|
||||
</Styled.DescriptionText>
|
||||
{ hasCheckbox ? (
|
||||
<label htmlFor="confirmationCheckbox" key="confirmation-checkbox">
|
||||
<Styled.Label htmlFor="confirmationCheckbox" key="confirmation-checkbox">
|
||||
<Styled.Checkbox
|
||||
type="checkbox"
|
||||
id="confirmationCheckbox"
|
||||
@ -84,7 +84,7 @@ class ConfirmationModal extends Component {
|
||||
aria-label={intl.formatMessage({ id: checkboxMessageId })}
|
||||
/>
|
||||
<span aria-hidden>{intl.formatMessage({ id: checkboxMessageId })}</span>
|
||||
</label>
|
||||
</Styled.Label>
|
||||
) : null }
|
||||
</Styled.Description>
|
||||
|
||||
|
@ -83,6 +83,10 @@ const ConfirmationButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.label`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export default {
|
||||
ConfirmationModal,
|
||||
Container,
|
||||
@ -93,4 +97,5 @@ export default {
|
||||
Checkbox,
|
||||
Footer,
|
||||
ConfirmationButton,
|
||||
Label,
|
||||
};
|
||||
|
@ -3,12 +3,14 @@ import { ChatContextProvider } from '/imports/ui/components/components-data/chat
|
||||
import { UsersContextProvider } from '/imports/ui/components/components-data/users-context/context';
|
||||
import { GroupChatContextProvider } from '/imports/ui/components/components-data/group-chat-context/context';
|
||||
import { LayoutContextProvider } from '/imports/ui/components/layout/context';
|
||||
import { CustomBackgroundsProvider } from '/imports/ui/components/video-preview/virtual-background/context';
|
||||
|
||||
const providersList = [
|
||||
ChatContextProvider,
|
||||
GroupChatContextProvider,
|
||||
UsersContextProvider,
|
||||
LayoutContextProvider,
|
||||
CustomBackgroundsProvider,
|
||||
];
|
||||
|
||||
const ContextProvidersComponent = props => providersList.reduce((acc, Component) => (
|
||||
|
@ -48,6 +48,10 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
webcamEffectsTitle: {
|
||||
id: 'app.videoPreview.webcamEffectsTitle',
|
||||
description: 'Title for the video effects modal',
|
||||
},
|
||||
webcamSettingsTitle: {
|
||||
id: 'app.videoPreview.webcamSettingsTitle',
|
||||
description: 'Title for the video preview modal',
|
||||
@ -315,9 +319,9 @@ class VideoPreview extends Component {
|
||||
}
|
||||
|
||||
// Resolves into true if the background switch is successful, false otherwise
|
||||
handleVirtualBgSelected(type, name) {
|
||||
handleVirtualBgSelected(type, name, customParams) {
|
||||
if (type !== EFFECT_TYPES.NONE_TYPE) {
|
||||
return this.startVirtualBackground(this.currentVideoStream, type, name);
|
||||
return this.startVirtualBackground(this.currentVideoStream, type, name, customParams);
|
||||
} else {
|
||||
this.stopVirtualBackground(this.currentVideoStream);
|
||||
return Promise.resolve(true);
|
||||
@ -331,12 +335,12 @@ class VideoPreview extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
startVirtualBackground(bbbVideoStream, type, name) {
|
||||
startVirtualBackground(bbbVideoStream, type, name, customParams) {
|
||||
this.setState({ isStartSharingDisabled: true });
|
||||
|
||||
if (bbbVideoStream == null) return Promise.resolve(false);
|
||||
|
||||
return bbbVideoStream.startVirtualBackground(type, name).then(() => {
|
||||
return bbbVideoStream.startVirtualBackground(type, name, customParams).then(() => {
|
||||
this.displayPreview();
|
||||
return true;
|
||||
}).catch(error => {
|
||||
@ -561,6 +565,7 @@ class VideoPreview extends Component {
|
||||
const {
|
||||
intl,
|
||||
sharedDevices,
|
||||
isVisualEffects,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -571,6 +576,14 @@ class VideoPreview extends Component {
|
||||
|
||||
const shared = sharedDevices.includes(webcamDeviceId);
|
||||
|
||||
if (isVisualEffects) {
|
||||
return (
|
||||
<Styled.Col>
|
||||
{isVirtualBackgroundsEnabled() && this.renderVirtualBgSelector()}
|
||||
</Styled.Col>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Col>
|
||||
<Styled.Label htmlFor="setCam">
|
||||
@ -642,6 +655,7 @@ class VideoPreview extends Component {
|
||||
}
|
||||
|
||||
renderVirtualBgSelector() {
|
||||
const { isVisualEffects } = this.props;
|
||||
const { isStartSharingDisabled, webcamDeviceId } = this.state;
|
||||
const initialVirtualBgState = this.currentVideoStream ? {
|
||||
type: this.currentVideoStream.virtualBgType,
|
||||
@ -654,6 +668,7 @@ class VideoPreview extends Component {
|
||||
locked={isStartSharingDisabled}
|
||||
showThumbnails={SHOW_THUMBNAILS}
|
||||
initialVirtualBgState={initialVirtualBgState}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -725,6 +740,7 @@ class VideoPreview extends Component {
|
||||
hasVideoStream,
|
||||
forceOpen,
|
||||
camCapReached,
|
||||
isVisualEffects,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -741,6 +757,10 @@ class VideoPreview extends Component {
|
||||
|
||||
const { isIe } = browserInfo;
|
||||
|
||||
const title = isVisualEffects
|
||||
? intl.formatMessage(intlMessages.webcamEffectsTitle)
|
||||
: intl.formatMessage(intlMessages.webcamSettingsTitle);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isIe ? (
|
||||
@ -756,37 +776,39 @@ class VideoPreview extends Component {
|
||||
</Styled.BrowserWarning>
|
||||
) : null}
|
||||
<Styled.Title>
|
||||
{intl.formatMessage(intlMessages.webcamSettingsTitle)}
|
||||
{title}
|
||||
</Styled.Title>
|
||||
|
||||
{this.renderContent()}
|
||||
|
||||
<Styled.Footer>
|
||||
{hasVideoStream && VideoService.isMultipleCamerasEnabled()
|
||||
? (
|
||||
<Styled.ExtraActions>
|
||||
<Button
|
||||
color="danger"
|
||||
label={intl.formatMessage(intlMessages.stopSharingAllLabel)}
|
||||
onClick={this.handleStopSharingAll}
|
||||
disabled={shouldDisableButtons}
|
||||
/>
|
||||
</Styled.ExtraActions>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<Styled.Actions>
|
||||
{!shared && camCapReached ? (
|
||||
<span>{intl.formatMessage(intlMessages.camCapReached)}</span>
|
||||
) : (<Button
|
||||
data-test="startSharingWebcam"
|
||||
color={shared ? 'danger' : 'primary'}
|
||||
label={intl.formatMessage(shared ? intlMessages.stopSharingLabel : intlMessages.startSharingLabel)}
|
||||
onClick={shared ? this.handleStopSharing : this.handleStartSharing}
|
||||
disabled={isStartSharingDisabled || isStartSharingDisabled === null || shouldDisableButtons}
|
||||
/>)}
|
||||
</Styled.Actions>
|
||||
</Styled.Footer>
|
||||
{!isVisualEffects ? (
|
||||
<Styled.Footer>
|
||||
{hasVideoStream && VideoService.isMultipleCamerasEnabled()
|
||||
? (
|
||||
<Styled.ExtraActions>
|
||||
<Button
|
||||
color="danger"
|
||||
label={intl.formatMessage(intlMessages.stopSharingAllLabel)}
|
||||
onClick={this.handleStopSharingAll}
|
||||
disabled={shouldDisableButtons}
|
||||
/>
|
||||
</Styled.ExtraActions>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<Styled.Actions>
|
||||
{!shared && camCapReached ? (
|
||||
<span>{intl.formatMessage(intlMessages.camCapReached)}</span>
|
||||
) : (<Button
|
||||
data-test="startSharingWebcam"
|
||||
color={shared ? 'danger' : 'primary'}
|
||||
label={intl.formatMessage(shared ? intlMessages.stopSharingLabel : intlMessages.startSharingLabel)}
|
||||
onClick={shared ? this.handleStopSharing : this.handleStartSharing}
|
||||
disabled={isStartSharingDisabled || isStartSharingDisabled === null || shouldDisableButtons}
|
||||
/>)}
|
||||
</Styled.Actions>
|
||||
</Styled.Footer>
|
||||
) : null }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useContext, useEffect } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -10,6 +10,8 @@ import {
|
||||
getVirtualBackgroundThumbnail,
|
||||
isVirtualBackgroundSupported,
|
||||
} from '/imports/ui/services/virtual-background/service';
|
||||
import { CustomVirtualBackgroundsContext } from './context';
|
||||
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
@ -37,6 +39,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.video.virtualBackground.none',
|
||||
description: 'Label for no virtual background selected',
|
||||
},
|
||||
customLabel: {
|
||||
id: 'app.video.virtualBackground.custom',
|
||||
description: 'Label for custom virtual background selected',
|
||||
},
|
||||
removeLabel: {
|
||||
id: 'app.video.virtualBackground.remove',
|
||||
description: 'Label for remove custom virtual background',
|
||||
},
|
||||
blurLabel: {
|
||||
id: 'app.video.virtualBackground.blur',
|
||||
description: 'Label for the blurred camera option',
|
||||
@ -49,6 +59,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.video.virtualBackground.background',
|
||||
description: 'Label for the background word',
|
||||
},
|
||||
backgroundWithIndex: {
|
||||
id: 'app.video.virtualBackground.backgroundWithIndex',
|
||||
description: 'Label for the background word indexed',
|
||||
},
|
||||
...IMAGE_NAMES.reduce((prev, imageName) => {
|
||||
const id = imageName.split('.').shift();
|
||||
return {
|
||||
@ -68,15 +82,31 @@ const VirtualBgSelector = ({
|
||||
locked,
|
||||
showThumbnails,
|
||||
initialVirtualBgState,
|
||||
isVisualEffects,
|
||||
}) => {
|
||||
const [currentVirtualBg, setCurrentVirtualBg] = useState({
|
||||
...initialVirtualBgState,
|
||||
});
|
||||
|
||||
const inputElementsRef = useRef([]);
|
||||
const customBgSelectorRef = useRef(null);
|
||||
|
||||
const _virtualBgSelected = (type, name, index) =>
|
||||
handleVirtualBgSelected(type, name)
|
||||
const {
|
||||
dispatch,
|
||||
loaded,
|
||||
customBackgrounds,
|
||||
newCustomBackgrounds,
|
||||
loadFromDB,
|
||||
} = useContext(CustomVirtualBackgroundsContext);
|
||||
|
||||
const { MIME_TYPES_ALLOWED } = VirtualBgService;
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded && isVisualEffects) loadFromDB();
|
||||
}, []);
|
||||
|
||||
const _virtualBgSelected = (type, name, index, customParams) =>
|
||||
handleVirtualBgSelected(type, name, customParams)
|
||||
.then(switched => {
|
||||
// Reset to the base NONE_TYPE effect if it failed because the expected
|
||||
// behaviour from upstream's method is to actually stop/reset the effect
|
||||
@ -128,6 +158,24 @@ const VirtualBgSelector = ({
|
||||
);
|
||||
}
|
||||
|
||||
const handleCustomBgChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
const { readFile } = VirtualBgService;
|
||||
|
||||
readFile(
|
||||
file,
|
||||
(background) => {
|
||||
dispatch({
|
||||
type: 'new',
|
||||
background,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
// Add some logging, notification, etc.
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const renderThumbnailSelector = () => {
|
||||
const disabled = locked || !isVirtualBackgroundSupported();
|
||||
|
||||
@ -136,6 +184,7 @@ const VirtualBgSelector = ({
|
||||
<Styled.BgWrapper
|
||||
role="group"
|
||||
aria-label={intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)}
|
||||
isVisualEffects={isVisualEffects}
|
||||
>
|
||||
<>
|
||||
<Styled.BgNoneButton
|
||||
@ -147,6 +196,7 @@ const VirtualBgSelector = ({
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled}
|
||||
onClick={() => _virtualBgSelected(EFFECT_TYPES.NONE_TYPE)}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
<div aria-hidden className="sr-only" id={`vr-cam-btn-none`}>
|
||||
{intl.formatMessage(intlMessages.camBgAriaDesc, { 0: EFFECT_TYPES.NONE_TYPE })}
|
||||
@ -165,6 +215,7 @@ const VirtualBgSelector = ({
|
||||
disabled={disabled}
|
||||
ref={ref => { inputElementsRef.current[0] = ref; }}
|
||||
onClick={() => _virtualBgSelected(EFFECT_TYPES.BLUR_TYPE, 'Blur', 0)}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
<div aria-hidden className="sr-only" id={`vr-cam-btn-blur`}>
|
||||
{intl.formatMessage(intlMessages.camBgAriaDesc, { 0: EFFECT_TYPES.BLUR_TYPE })}
|
||||
@ -191,6 +242,7 @@ const VirtualBgSelector = ({
|
||||
ref={ref => inputElementsRef.current[index + 1] = ref}
|
||||
onClick={() => _virtualBgSelected(EFFECT_TYPES.IMAGE_TYPE, imageName, index + 1)}
|
||||
disabled={disabled}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
<Styled.Thumbnail onClick={() => {
|
||||
const node = findDOMNode(inputElementsRef.current[index + 1]);
|
||||
@ -203,6 +255,95 @@ const VirtualBgSelector = ({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{isVisualEffects && customBackgrounds
|
||||
.concat(newCustomBackgrounds)
|
||||
.map(({ filename, data, uniqueId }, index) => {
|
||||
const imageIndex = index + IMAGE_NAMES.length + 2;
|
||||
const label = intl.formatMessage(intlMessages.backgroundWithIndex, {
|
||||
0: imageIndex,
|
||||
});
|
||||
|
||||
return (
|
||||
<Styled.ThumbnailButtonWrapper key={`${filename}-${index}`}>
|
||||
<Styled.ThumbnailButton
|
||||
id={`${filename}-${imageIndex}`}
|
||||
label={label}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
aria-describedby={`vr-cam-btn-${imageIndex}`}
|
||||
aria-pressed={currentVirtualBg?.name?.includes(filename)}
|
||||
hideLabel
|
||||
ref={ref => inputElementsRef.current[index + IMAGE_NAMES.length + 1] = ref}
|
||||
onClick={() => _virtualBgSelected(
|
||||
EFFECT_TYPES.IMAGE_TYPE,
|
||||
filename,
|
||||
imageIndex - 1,
|
||||
{ file: data },
|
||||
)}
|
||||
disabled={disabled}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
<Styled.Thumbnail onClick={() => {
|
||||
const node = findDOMNode(inputElementsRef.current[index + IMAGE_NAMES.length + 1]);
|
||||
node.focus();
|
||||
node.click();
|
||||
}} aria-hidden src={data} />
|
||||
<Styled.ButtonWrapper>
|
||||
<Styled.ButtonRemove
|
||||
label={intl.formatMessage(intlMessages.removeLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.removeLabel)}
|
||||
data-test="removeCustomBackground"
|
||||
icon="close"
|
||||
size="sm"
|
||||
color="dark"
|
||||
circle
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'delete',
|
||||
uniqueId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Styled.ButtonWrapper>
|
||||
<div aria-hidden className="sr-only" id={`vr-cam-btn-${imageIndex}`}>
|
||||
{label}
|
||||
</div>
|
||||
</Styled.ThumbnailButtonWrapper>
|
||||
);
|
||||
})}
|
||||
|
||||
{isVisualEffects && (
|
||||
<>
|
||||
<Styled.BgCustomButton
|
||||
icon='plus'
|
||||
label={intl.formatMessage(intlMessages.customLabel)}
|
||||
aria-describedby={`vr-cam-btn-custom`}
|
||||
hideLabel
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (customBgSelectorRef.current) {
|
||||
customBgSelectorRef.current.click();
|
||||
}
|
||||
}}
|
||||
isVisualEffects={isVisualEffects}
|
||||
/>
|
||||
<input
|
||||
ref={customBgSelectorRef}
|
||||
type="file"
|
||||
id="customBgSelector"
|
||||
onChange={handleCustomBgChange}
|
||||
style={{ display: 'none' }}
|
||||
accept={MIME_TYPES_ALLOWED.join(', ')}
|
||||
/>
|
||||
<div aria-hidden className="sr-only" id={`vr-cam-btn-custom`}>
|
||||
{intl.formatMessage(intlMessages.customLabel)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Styled.BgWrapper>
|
||||
</Styled.VirtualBackgroundRowThumbnail>
|
||||
);
|
||||
@ -215,11 +356,13 @@ const VirtualBgSelector = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Styled.Label>
|
||||
{!isVirtualBackgroundSupported()
|
||||
? intl.formatMessage(intlMessages.virtualBackgroundSettingsDisabledLabel)
|
||||
: intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)}
|
||||
</Styled.Label>
|
||||
{!isVisualEffects && (
|
||||
<Styled.Label>
|
||||
{!isVirtualBackgroundSupported()
|
||||
? intl.formatMessage(intlMessages.virtualBackgroundSettingsDisabledLabel)
|
||||
: intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)}
|
||||
</Styled.Label>
|
||||
)}
|
||||
|
||||
{renderSelector()}
|
||||
</>
|
||||
|
@ -0,0 +1,82 @@
|
||||
import React, { useReducer } from 'react';
|
||||
import _ from 'lodash';
|
||||
import Service from './service';
|
||||
|
||||
export const CustomVirtualBackgroundsContext = React.createContext();
|
||||
|
||||
const reducer = (state, action) => {
|
||||
const { save, del } = Service;
|
||||
|
||||
switch (action.type) {
|
||||
case 'load': {
|
||||
return {
|
||||
...state,
|
||||
loaded: true,
|
||||
customBackgrounds: action.backgrounds,
|
||||
newCustomBackgrounds: [],
|
||||
};
|
||||
}
|
||||
case 'new': {
|
||||
save(action.background);
|
||||
return {
|
||||
...state,
|
||||
newCustomBackgrounds: [
|
||||
...state.newCustomBackgrounds,
|
||||
action.background,
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'delete': {
|
||||
const { customBackgrounds, newCustomBackgrounds } = state;
|
||||
const filterFunc = ({ uniqueId }) => uniqueId !== action.uniqueId;
|
||||
|
||||
del(action.uniqueId);
|
||||
return {
|
||||
...state,
|
||||
customBackgrounds: customBackgrounds.filter(filterFunc),
|
||||
newCustomBackgrounds: newCustomBackgrounds.filter(filterFunc),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown custom background action.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomBackgroundsProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
loaded: false,
|
||||
customBackgrounds: [],
|
||||
newCustomBackgrounds: [],
|
||||
});
|
||||
|
||||
const { load } = Service;
|
||||
|
||||
const loadFromDB = () => {
|
||||
const onError = () => dispatch({
|
||||
type: 'load',
|
||||
backgrounds: [],
|
||||
});
|
||||
|
||||
const onSuccess = (backgrounds) => dispatch({
|
||||
type: 'load',
|
||||
backgrounds,
|
||||
});
|
||||
|
||||
load(onError, onSuccess);
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomVirtualBackgroundsContext.Provider
|
||||
value={{
|
||||
dispatch,
|
||||
loaded: state.loaded,
|
||||
customBackgrounds: state.customBackgrounds,
|
||||
newCustomBackgrounds: state.newCustomBackgrounds,
|
||||
loadFromDB: _.throttle(loadFromDB, 500, { leading: true, trailing: false }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CustomVirtualBackgroundsContext.Provider>
|
||||
);
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import _ from 'lodash';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
|
||||
const MIME_TYPES_ALLOWED = ['image/png', 'image/jpeg'];
|
||||
const MAX_FILE_SIZE = 5000; // KBytes
|
||||
|
||||
const withObjectStore = ({
|
||||
onError,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const request = window.indexedDB.open('BBB', 1);
|
||||
|
||||
request.onerror = onError;
|
||||
|
||||
request.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
db.createObjectStore('CustomBackgrounds', { keyPath: 'uniqueId' });
|
||||
};
|
||||
|
||||
request.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction(['CustomBackgrounds'], 'readwrite');
|
||||
const objectStore = transaction.objectStore('CustomBackgrounds');
|
||||
|
||||
onSuccess(objectStore);
|
||||
};
|
||||
};
|
||||
|
||||
const genericErrorHandlerBuilder = (
|
||||
code,
|
||||
errorMessage,
|
||||
notifyMessage,
|
||||
callback,
|
||||
) => (e) => {
|
||||
notify(notifyMessage, 'error', 'warning');
|
||||
logger.warn({
|
||||
logCode: code,
|
||||
extraInfo: {
|
||||
errorName: e.name,
|
||||
errorMessage: e.message,
|
||||
},
|
||||
}, `${errorMessage}: ${e.message}`);
|
||||
|
||||
if (callback) callback(e);
|
||||
};
|
||||
|
||||
const load = (onError, onSuccess) => {
|
||||
withObjectStore({
|
||||
onError: genericErrorHandlerBuilder(
|
||||
'IndexedDB_access',
|
||||
'Error on load custom backgrounds to IndexedDB',
|
||||
'Something wrong while loading custom backgrounds',
|
||||
onError,
|
||||
),
|
||||
onSuccess: (objectStore) => {
|
||||
const backgrounds = [];
|
||||
|
||||
objectStore.openCursor().onsuccess = (e) => {
|
||||
const cursor = e.target.result;
|
||||
|
||||
if (cursor) {
|
||||
backgrounds.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
onSuccess(backgrounds);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const save = (background) => {
|
||||
withObjectStore({
|
||||
onError: genericErrorHandlerBuilder(
|
||||
'IndexedDB_access',
|
||||
'Error on save custom background to IndexedDB',
|
||||
'Something wrong while saving custom background',
|
||||
),
|
||||
onSuccess: (objectStore) => {
|
||||
objectStore.add(background);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const del = (key) => {
|
||||
withObjectStore({
|
||||
onError: genericErrorHandlerBuilder(
|
||||
'IndexedDB_access',
|
||||
'Error on delete custom background to IndexedDB',
|
||||
'Something wrong while deleting custom background',
|
||||
),
|
||||
onSuccess: (objectStore) => {
|
||||
objectStore.delete(key);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const parseFilename = (filename = '') => {
|
||||
const substrings = filename.split('.');
|
||||
substrings.pop();
|
||||
const filenameWithoutExtension = substrings.join('');
|
||||
return filenameWithoutExtension;
|
||||
};
|
||||
|
||||
const readFile = (file, onSuccess, onError) => {
|
||||
const { name, size, type } = file;
|
||||
const sizeInKB = size / 1024;
|
||||
|
||||
if (sizeInKB > MAX_FILE_SIZE) {
|
||||
return onError(new Error('Maximum file size exceeded.'));
|
||||
}
|
||||
|
||||
if (!MIME_TYPES_ALLOWED.includes(type)) {
|
||||
return onError(new Error('File type not allowed.'));
|
||||
}
|
||||
|
||||
const filenameWithoutExtension = parseFilename(name);
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
const background = {
|
||||
filename: filenameWithoutExtension,
|
||||
data: e.target.result,
|
||||
uniqueId: _.uniqueId(),
|
||||
};
|
||||
onSuccess(background);
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
export default {
|
||||
load,
|
||||
save,
|
||||
del,
|
||||
readFile,
|
||||
MIME_TYPES_ALLOWED,
|
||||
MAX_FILE_SIZE,
|
||||
};
|
@ -3,6 +3,7 @@ import {
|
||||
borderSize,
|
||||
borderSizeLarge,
|
||||
borderSizeSmall,
|
||||
smPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
userThumbnailBorder,
|
||||
@ -13,18 +14,26 @@ import {
|
||||
colorPrimary,
|
||||
colorWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
import { fontSizeSmallest } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const VirtualBackgroundRowThumbnail = styled.div`
|
||||
margin-top: 0.4rem;
|
||||
`;
|
||||
|
||||
const BgWrapper = styled.div`
|
||||
const BgWrapper = styled(ScrollboxVertical)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
margin: ${borderSizeLarge};
|
||||
padding: ${borderSizeLarge};
|
||||
|
||||
${({ isVisualEffects }) => isVisualEffects && `
|
||||
height: 15rem;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
`}
|
||||
`;
|
||||
|
||||
const BgNoneButton = styled(Button)`
|
||||
@ -34,6 +43,10 @@ const BgNoneButton = styled(Button)`
|
||||
border: ${borderSizeSmall} solid ${userThumbnailBorder};
|
||||
margin: 0 0.15em;
|
||||
flex-shrink: 0;
|
||||
|
||||
${({ isVisualEffects }) => isVisualEffects && `
|
||||
margin: 0.15em;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ThumbnailButton = styled(Button)`
|
||||
@ -52,6 +65,12 @@ const ThumbnailButton = styled(Button)`
|
||||
margin: 0 0.15em;
|
||||
flex-shrink: 0;
|
||||
|
||||
${({ isVisualEffects }) => isVisualEffects && `
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin: 0.15em;
|
||||
`}
|
||||
|
||||
& + img {
|
||||
border-radius: ${borderSizeLarge};
|
||||
}
|
||||
@ -112,6 +131,26 @@ const Label = styled.label`
|
||||
color: ${colorGrayLabel};
|
||||
`;
|
||||
|
||||
const ThumbnailButtonWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
const ButtonRemove = styled(Button)`
|
||||
span {
|
||||
font-size: ${fontSizeSmallest};
|
||||
padding: ${smPaddingY};
|
||||
}
|
||||
`;
|
||||
|
||||
const BgCustomButton = styled(BgNoneButton)``;
|
||||
|
||||
export default {
|
||||
VirtualBackgroundRowThumbnail,
|
||||
BgWrapper,
|
||||
@ -120,4 +159,8 @@ export default {
|
||||
Thumbnail,
|
||||
Select,
|
||||
Label,
|
||||
ThumbnailButtonWrapper,
|
||||
ButtonWrapper,
|
||||
ButtonRemove,
|
||||
BgCustomButton,
|
||||
};
|
@ -147,6 +147,7 @@ class VideoProvider extends Component {
|
||||
VideoService.getPageChangeDebounceTime(),
|
||||
{ leading: false, trailing: true },
|
||||
);
|
||||
this.startVirtualBackgroundByDrop = this.startVirtualBackgroundByDrop.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -937,6 +938,34 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
startVirtualBackgroundByDrop(stream, type, name, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const { bbbVideoStream } = peer;
|
||||
const video = this.getVideoElement(stream);
|
||||
|
||||
if (peer && video && peer.attached && video.srcObject) {
|
||||
bbbVideoStream.startVirtualBackground(type, name, { file: data })
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleVirtualBgErrorByDropping(error, type, name);
|
||||
});
|
||||
}
|
||||
|
||||
handleVirtualBgErrorByDropping(error, type, name) {
|
||||
logger.error({
|
||||
logCode: `video_provider_virtualbg_error`,
|
||||
extraInfo: {
|
||||
errorName: error.name,
|
||||
errorMessage: error.message,
|
||||
virtualBgType: type,
|
||||
virtualBgName: name,
|
||||
},
|
||||
}, `Failed to start virtual background by dropping image: ${error.message}`);
|
||||
}
|
||||
|
||||
restoreVirtualBackground(stream, type, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (type !== EFFECT_TYPES.NONE_TYPE) {
|
||||
@ -1116,6 +1145,7 @@ class VideoProvider extends Component {
|
||||
}}
|
||||
onVideoItemMount={this.createVideoTag}
|
||||
onVideoItemUnmount={this.destroyVideoTag}
|
||||
onVirtualBgDrop={this.startVirtualBackgroundByDrop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,20 @@ import Styled from './styles';
|
||||
import { validIOSVersion } from '/imports/ui/components/app/service';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { debounce } from 'lodash';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
|
||||
const ENABLE_WEBCAM_SELECTOR_BUTTON = Meteor.settings.public.app.enableWebcamSelectorButton;
|
||||
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = Meteor.settings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
videoSettings: {
|
||||
id: 'app.video.videoSettings',
|
||||
description: 'Open video settings',
|
||||
},
|
||||
visualEffects: {
|
||||
id: 'app.video.visualEffects',
|
||||
description: 'Visual effects label',
|
||||
},
|
||||
joinVideo: {
|
||||
id: 'app.video.joinVideo',
|
||||
description: 'Join video button label',
|
||||
@ -66,6 +76,9 @@ const JoinVideoButton = ({
|
||||
const isDesktopSharingCamera = hasVideoStream && !isMobile;
|
||||
const shouldEnableWebcamSelectorButton = ENABLE_WEBCAM_SELECTOR_BUTTON
|
||||
&& isDesktopSharingCamera;
|
||||
const shouldEnableWebcamBackgroundUploadButton = ENABLE_WEBCAM_BACKGROUND_UPLOAD
|
||||
&& hasVideoStream
|
||||
&& !isMobile;
|
||||
const exitVideo = () => isDesktopSharingCamera && (!VideoService.isMultipleCamerasEnabled()
|
||||
|| shouldEnableWebcamSelectorButton);
|
||||
|
||||
@ -88,9 +101,8 @@ const JoinVideoButton = ({
|
||||
}
|
||||
}, JOIN_VIDEO_DELAY_MILLISECONDS);
|
||||
|
||||
const handleOpenAdvancedOptions = (e) => {
|
||||
e.stopPropagation();
|
||||
mountVideoPreview(isMobileSharingCamera);
|
||||
const handleOpenAdvancedOptions = (props) => {
|
||||
mountVideoPreview(isMobileSharingCamera, props);
|
||||
};
|
||||
|
||||
const getMessageFromStatus = () => {
|
||||
@ -107,17 +119,44 @@ const JoinVideoButton = ({
|
||||
|
||||
const isSharing = hasVideoStream || status === 'videoConnecting';
|
||||
|
||||
const renderEmojiButton = () => (
|
||||
shouldEnableWebcamSelectorButton
|
||||
&& (
|
||||
<ButtonEmoji
|
||||
onClick={handleOpenAdvancedOptions}
|
||||
emoji="device_list_selector"
|
||||
hideLabel
|
||||
label={intl.formatMessage(intlMessages.advancedVideo)}
|
||||
const renderUserActions = () => {
|
||||
const actions = [];
|
||||
|
||||
if (shouldEnableWebcamSelectorButton) {
|
||||
actions.push(
|
||||
{
|
||||
key: 'advancedVideo',
|
||||
label: intl.formatMessage(intlMessages.advancedVideo),
|
||||
onClick: () => handleOpenAdvancedOptions(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldEnableWebcamBackgroundUploadButton) {
|
||||
actions.push(
|
||||
{
|
||||
key: 'virtualBgSelection',
|
||||
label: intl.formatMessage(intlMessages.visualEffects),
|
||||
onClick: () => handleOpenAdvancedOptions({ isVisualEffects: true }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (actions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<ButtonEmoji
|
||||
emoji="device_list_selector"
|
||||
hideLabel
|
||||
label={intl.formatMessage(intlMessages.videoSettings)}
|
||||
/>
|
||||
)}
|
||||
actions={actions}
|
||||
/>
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.OffsetBottom>
|
||||
@ -133,7 +172,7 @@ const JoinVideoButton = ({
|
||||
circle
|
||||
disabled={!!disableReason}
|
||||
/>
|
||||
{renderEmojiButton()}
|
||||
{renderUserActions()}
|
||||
</Styled.OffsetBottom>
|
||||
);
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ const JoinVideoOptionsContainer = (props) => {
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const mountVideoPreview = (force) => { mountModal(<VideoPreviewContainer forceOpen={force} />); };
|
||||
const mountVideoPreview = (force, props) => { mountModal(<VideoPreviewContainer forceOpen={force} {...(props || {})} />); };
|
||||
|
||||
return (
|
||||
<JoinVideoButton {...{
|
||||
|
@ -291,6 +291,7 @@ class VideoList extends Component {
|
||||
renderVideoList() {
|
||||
const {
|
||||
streams,
|
||||
onVirtualBgDrop,
|
||||
onVideoItemMount,
|
||||
onVideoItemUnmount,
|
||||
swapLayout,
|
||||
@ -322,6 +323,7 @@ class VideoList extends Component {
|
||||
}}
|
||||
onVideoItemUnmount={onVideoItemUnmount}
|
||||
swapLayout={swapLayout}
|
||||
onVirtualBgDrop={(type, name, data) => onVirtualBgDrop(stream, type, name, data)}
|
||||
/>
|
||||
</Styled.VideoListItem>
|
||||
);
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import ViewActions from '/imports/ui/components/video-provider/video-list/video-list-item/view-actions/component';
|
||||
import UserActions from '/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component';
|
||||
import UserStatus from '/imports/ui/components/video-provider/video-list/video-list-item/user-status/component';
|
||||
import PinArea from '/imports/ui/components/video-provider/video-list/video-list-item/pin-area/component';
|
||||
import UserAvatarVideo from '/imports/ui/components/video-provider/video-list/video-list-item/user-avatar/component';
|
||||
import ViewActions from '/imports/ui/components/video-provider/video-list/video-list-item/view-actions/component';
|
||||
import {
|
||||
isStreamStateUnhealthy,
|
||||
subscribeToStreamStateChange,
|
||||
@ -14,13 +15,15 @@ import {
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import VideoService from '/imports/ui/components/video-provider/service';
|
||||
import Styled from './styles';
|
||||
import { withDragAndDrop } from './drag-and-drop/component';
|
||||
|
||||
const VIDEO_CONTAINER_WIDTH_BOUND = 125;
|
||||
|
||||
const VideoListItem = (props) => {
|
||||
const {
|
||||
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
|
||||
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount,
|
||||
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, onVirtualBgDrop,
|
||||
makeDragOperations,
|
||||
} = props;
|
||||
|
||||
const [videoIsReady, setVideoIsReady] = useState(false);
|
||||
@ -198,6 +201,7 @@ const VideoListItem = (props) => {
|
||||
fullscreen={isFullscreenContext}
|
||||
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
|
||||
animations={animations}
|
||||
{...makeDragOperations(onVirtualBgDrop, user.userId)}
|
||||
>
|
||||
<Styled.VideoContainer>
|
||||
<Styled.Video
|
||||
@ -224,7 +228,7 @@ const VideoListItem = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(VideoListItem);
|
||||
export default withDragAndDrop(injectIntl(VideoListItem));
|
||||
|
||||
VideoListItem.defaultProps = {
|
||||
numOfStreams: 0,
|
||||
|
@ -0,0 +1,141 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
|
||||
import { CustomVirtualBackgroundsContext } from '/imports/ui/components/video-preview/virtual-background/context';
|
||||
import { EFFECT_TYPES } from '/imports/ui/services/virtual-background/service';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
confirmationTitle: {
|
||||
id: 'app.confirmation.virtualBackground.title',
|
||||
description: 'Confirmation modal title',
|
||||
},
|
||||
confirmationDescription: {
|
||||
id: 'app.confirmation.virtualBackground.description',
|
||||
description: 'Confirmation modal description',
|
||||
},
|
||||
});
|
||||
|
||||
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = Meteor.settings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
|
||||
|
||||
const DragAndDrop = (props) => {
|
||||
const { children, mountModal, intl } = props;
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [draggingOver, setDraggingOver] = useState(false);
|
||||
const { dispatch: dispatchCustomBackground } = useContext(CustomVirtualBackgroundsContext);
|
||||
|
||||
const resetEvent = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onDragOver = (e) => {
|
||||
resetEvent(e);
|
||||
setDragging(true);
|
||||
};
|
||||
const onDragLeave = (e) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
};
|
||||
const onDrop = (e) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
window.addEventListener('dragover', onDragOver);
|
||||
window.addEventListener('dragleave', onDragLeave);
|
||||
window.addEventListener('drop', onDrop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('dragover', onDragOver);
|
||||
window.removeEventListener('dragleave', onDragLeave);
|
||||
window.removeEventListener('drop', onDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const makeDragOperations = (onAction, userId) => {
|
||||
if (Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD) return {};
|
||||
|
||||
const startAndSaveVirtualBackground = (file) => {
|
||||
const { readFile } = VirtualBgService;
|
||||
|
||||
readFile(
|
||||
file,
|
||||
(background) => {
|
||||
const { filename, data } = background;
|
||||
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
|
||||
dispatchCustomBackground({
|
||||
type: 'new',
|
||||
background,
|
||||
});
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
// Add some logging, notification, etc.
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onDragOverHandler = (e) => {
|
||||
resetEvent(e);
|
||||
setDraggingOver(true);
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
const onDropHandler = (e) => {
|
||||
resetEvent(e);
|
||||
setDraggingOver(false);
|
||||
setDragging(false);
|
||||
|
||||
const { files } = e.dataTransfer;
|
||||
const file = files[0];
|
||||
|
||||
if (Session.get('skipBackgroundDropConfirmation')) {
|
||||
return startAndSaveVirtualBackground(file);
|
||||
}
|
||||
|
||||
const onConfirm = (confirmParam, checked) => {
|
||||
startAndSaveVirtualBackground(file);
|
||||
Session.set('skipBackgroundDropConfirmation', checked);
|
||||
};
|
||||
|
||||
mountModal(
|
||||
<ConfirmationModal
|
||||
intl={intl}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage(intlMessages.confirmationTitle)}
|
||||
description={intl.formatMessage(intlMessages.confirmationDescription, { 0: file.name })}
|
||||
checkboxMessageId="app.confirmation.skipConfirm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onDragLeaveHandler = (e) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
setDraggingOver(false);
|
||||
}
|
||||
|
||||
return {
|
||||
onDragOver: onDragOverHandler,
|
||||
onDrop: onDropHandler,
|
||||
onDragLeave: onDragLeaveHandler,
|
||||
dragging,
|
||||
draggingOver,
|
||||
};
|
||||
}
|
||||
|
||||
return React.cloneElement(children, { ...props, makeDragOperations })
|
||||
};
|
||||
|
||||
const Wrapper = (Component) => (props) => (
|
||||
<DragAndDrop {...props} >
|
||||
<Component />
|
||||
</DragAndDrop>
|
||||
);
|
||||
|
||||
export const withDragAndDrop = (Component) => withModalMounter(injectIntl(Wrapper(Component)));
|
@ -4,6 +4,7 @@ import {
|
||||
colorBlack,
|
||||
colorWhite,
|
||||
webcamBackgroundColor,
|
||||
colorDanger,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { TextElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
|
||||
|
||||
@ -16,6 +17,15 @@ const rotate360 = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
const fade = keyframes`
|
||||
from {
|
||||
opacity: 0.7;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -41,6 +51,22 @@ const Content = styled.div`
|
||||
`}
|
||||
}
|
||||
|
||||
${({ dragging, animations }) => dragging && animations && css`
|
||||
&::after {
|
||||
animation: ${fade} .5s linear infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ dragging, draggingOver }) => (dragging || draggingOver) && `
|
||||
&::after {
|
||||
opacity: 0.7;
|
||||
border-style: dashed;
|
||||
border-color: ${colorDanger};
|
||||
transition: opacity 0s;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ fullscreen }) => fullscreen && `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -249,7 +249,11 @@ class VirtualBackgroundService {
|
||||
}
|
||||
this._virtualImage = document.createElement('img');
|
||||
this._virtualImage.crossOrigin = 'anonymous';
|
||||
this._virtualImage.src = virtualBackgroundImagePath + imagesrc;
|
||||
if (parameters.customParams) {
|
||||
this._virtualImage.src = parameters.customParams.file;
|
||||
} else {
|
||||
this._virtualImage.src = virtualBackgroundImagePath + imagesrc;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,7 +339,11 @@ export async function createVirtualBackgroundService(parameters = null) {
|
||||
parameters.backgroundType = 'blur';
|
||||
parameters.isVirtualBackground = false;
|
||||
} else {
|
||||
parameters.virtualSource = virtualBackgroundImagePath + parameters.backgroundFilename;
|
||||
if (parameters.customParams) {
|
||||
parameters.virtualSource = parameters.customParams.file;
|
||||
} else {
|
||||
parameters.virtualSource = virtualBackgroundImagePath + parameters.backgroundFilename;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modelResponse.ok) {
|
||||
|
@ -42,11 +42,12 @@ const {
|
||||
showThumbnails: SHOW_THUMBNAILS = true,
|
||||
} = Meteor.settings.public.virtualBackgrounds;
|
||||
|
||||
const createVirtualBackgroundStream = (type, name, isVirtualBackground, stream) => {
|
||||
const createVirtualBackgroundStream = (type, name, isVirtualBackground, stream, customParams) => {
|
||||
const buildParams = {
|
||||
backgroundType: type,
|
||||
backgroundFilename: name,
|
||||
isVirtualBackground,
|
||||
customParams,
|
||||
}
|
||||
|
||||
return createVirtualBackgroundService(buildParams).then((service) => {
|
||||
|
@ -76,12 +76,13 @@ export class BBBVideoStream extends EventEmitter2 {
|
||||
BBBVideoStream.trackStreamTermination(this.originalStream, notify);
|
||||
}
|
||||
|
||||
_changeVirtualBackground (type, name) {
|
||||
_changeVirtualBackground (type, name, customParams) {
|
||||
try {
|
||||
this.virtualBgService.changeBackgroundImage({
|
||||
type,
|
||||
name,
|
||||
isVirtualBackground: this.isVirtualBackground(type),
|
||||
customParams,
|
||||
});
|
||||
this.virtualBgType = type;
|
||||
this.virtualBgName = name;
|
||||
@ -92,14 +93,15 @@ export class BBBVideoStream extends EventEmitter2 {
|
||||
}
|
||||
|
||||
|
||||
startVirtualBackground (type, name = '') {
|
||||
if (this.virtualBgService) return this._changeVirtualBackground(type, name);
|
||||
startVirtualBackground (type, name = '', customParams) {
|
||||
if (this.virtualBgService) return this._changeVirtualBackground(type, name, customParams);
|
||||
|
||||
return createVirtualBackgroundStream(
|
||||
type,
|
||||
name,
|
||||
this.isVirtualBackground(type),
|
||||
this.mediaStream
|
||||
this.mediaStream,
|
||||
customParams,
|
||||
).then(({ service, effect }) => {
|
||||
this.virtualBgService = service;
|
||||
this.virtualBgType = type;
|
||||
|
@ -767,6 +767,7 @@ public:
|
||||
logTag: ''
|
||||
virtualBackgrounds:
|
||||
enabled: true
|
||||
enableVirtualBackgroundUpload: true
|
||||
storedOnBBB: true
|
||||
showThumbnails: true
|
||||
imagesPath: /resources/images/virtual-backgrounds/
|
||||
|
@ -76,6 +76,9 @@
|
||||
"app.captions.speech.start": "Speech recognition started",
|
||||
"app.captions.speech.stop": "Speech recognition stopped",
|
||||
"app.captions.speech.error": "Speech recognition stopped due to the browser incompatibility or some time of silence",
|
||||
"app.confirmation.skipConfirm": "Don't ask again",
|
||||
"app.confirmation.virtualBackground.title": "Start new virtual background",
|
||||
"app.confirmation.virtualBackground.description": "{0} will be added as virtual background. Continue?",
|
||||
"app.textInput.sendLabel": "Send",
|
||||
"app.title.defaultViewLabel": "Default presentation view",
|
||||
"app.notes.title": "Shared Notes",
|
||||
@ -803,6 +806,7 @@
|
||||
"app.videoPreview.webcamOptionLabel": "Choose webcam",
|
||||
"app.videoPreview.webcamPreviewLabel": "Webcam preview",
|
||||
"app.videoPreview.webcamSettingsTitle": "Webcam settings",
|
||||
"app.videoPreview.webcamEffectsTitle": "Webcam visual effects",
|
||||
"app.videoPreview.webcamVirtualBackgroundLabel": "Virtual background settings",
|
||||
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "This device does not support virtual backgrounds",
|
||||
"app.videoPreview.webcamNotFoundLabel": "Webcam not found",
|
||||
@ -810,6 +814,8 @@
|
||||
"app.video.joinVideo": "Share webcam",
|
||||
"app.video.connecting": "Webcam sharing is starting ...",
|
||||
"app.video.leaveVideo": "Stop sharing webcam",
|
||||
"app.video.videoSettings": "Video settings",
|
||||
"app.video.visualEffects": "Visual effects",
|
||||
"app.video.advancedVideo": "Open advanced settings",
|
||||
"app.video.iceCandidateError": "Error on adding ICE candidate",
|
||||
"app.video.iceConnectionStateError": "Connection failure (ICE error 1107)",
|
||||
@ -847,6 +853,9 @@
|
||||
"app.video.virtualBackground.board": "Board",
|
||||
"app.video.virtualBackground.coffeeshop": "Coffeeshop",
|
||||
"app.video.virtualBackground.background": "Background",
|
||||
"app.video.virtualBackground.backgroundWithIndex": "Background {0}",
|
||||
"app.video.virtualBackground.custom": "Upload from your computer",
|
||||
"app.video.virtualBackground.remove": "Remove added image",
|
||||
"app.video.virtualBackground.genericError": "Failed to apply camera effect. Try again.",
|
||||
"app.video.virtualBackground.camBgAriaDesc": "Sets webcam virtual background to {0}",
|
||||
"app.video.camCapReached": "You cannot share more cameras",
|
||||
|
Loading…
Reference in New Issue
Block a user