diff --git a/.eslintignore b/.eslintignore index e873e15c35..1e6b4b72b5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,7 +6,7 @@ build/* flow-typed/* libs/* resources/* -react/features/stream-effects/blur/vendor/* +react/features/stream-effects/virtual-background/vendor/* # ESLint will by default ignore its own configuration file. However, there does # not seem to be a reason why we will want to risk being inconsistent with our diff --git a/Makefile b/Makefile index ded3155802..0da02f74a2 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,8 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/ LIBFLAC_DIR = node_modules/libflacjs/dist/min/ OLM_DIR = node_modules/olm RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/ -TFLITE_WASM = react/features/stream-effects/blur/vendor/tflite -MEET_MODELS_DIR = react/features/stream-effects/blur/vendor/models/ +TFLITE_WASM = react/features/stream-effects/virtual-background/vendor/tflite +MEET_MODELS_DIR = react/features/stream-effects/virtual-background/vendor/models/ NODE_SASS = ./node_modules/.bin/sass NPM = npm OUTPUT_DIR = . @@ -51,8 +51,8 @@ deploy-appbundle: $(OUTPUT_DIR)/analytics-ga.js \ $(BUILD_DIR)/analytics-ga.min.js \ $(BUILD_DIR)/analytics-ga.min.map \ - $(BUILD_DIR)/video-blur-effect.min.js \ - $(BUILD_DIR)/video-blur-effect.min.map \ + $(BUILD_DIR)/virtual-background-effect.min.js \ + $(BUILD_DIR)/virtual-background-effect.min.map \ $(BUILD_DIR)/rnnoise-processor.min.js \ $(BUILD_DIR)/rnnoise-processor.min.map \ $(BUILD_DIR)/close3.min.js \ diff --git a/config.js b/config.js index f23503c2e9..eb0deb832c 100644 --- a/config.js +++ b/config.js @@ -430,7 +430,7 @@ var config = { // 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', // 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', // 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', - // 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' + // 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' // ], // Stats diff --git a/css/main.scss b/css/main.scss index 5dc10d112a..37d90d5ac7 100644 --- a/css/main.scss +++ b/css/main.scss @@ -43,6 +43,7 @@ $flagsImagePath: "../images/"; @import 'modals/settings/settings'; @import 'modals/speaker_stats/speaker_stats'; @import 'modals/video-quality/video-quality'; +@import 'modals/virtual-background/virtual-background'; @import 'modals/local-recording/local-recording'; @import 'videolayout_default'; @import 'notice'; diff --git a/css/modals/virtual-background/_virtual-background.scss b/css/modals/virtual-background/_virtual-background.scss new file mode 100644 index 0000000000..7055c47e26 --- /dev/null +++ b/css/modals/virtual-background/_virtual-background.scss @@ -0,0 +1,44 @@ +.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{ + font-weight: bold; + padding: 5px; + height: 35px; + width: 35px; + 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: 35px; + width: 35px; + border-radius: 10px; + border: 2px solid #a4b8d1; + text-align: center; + vertical-align: middle; + line-height: 35px; + margin-right: 5px; + } +} \ No newline at end of file diff --git a/images/virtual-background/background-1.jpg b/images/virtual-background/background-1.jpg new file mode 100644 index 0000000000..ce8ca50dc4 Binary files /dev/null and b/images/virtual-background/background-1.jpg differ diff --git a/images/virtual-background/background-2.jpg b/images/virtual-background/background-2.jpg new file mode 100644 index 0000000000..5d63c7a5ed Binary files /dev/null and b/images/virtual-background/background-2.jpg differ diff --git a/images/virtual-background/background-3.jpg b/images/virtual-background/background-3.jpg new file mode 100644 index 0000000000..42cfaa08b8 Binary files /dev/null and b/images/virtual-background/background-3.jpg differ diff --git a/images/virtual-background/background-4.jpg b/images/virtual-background/background-4.jpg new file mode 100644 index 0000000000..58912125d5 Binary files /dev/null and b/images/virtual-background/background-4.jpg differ diff --git a/interface_config.js b/interface_config.js index 70fd808590..b16794686e 100644 --- a/interface_config.js +++ b/interface_config.js @@ -206,7 +206,7 @@ var interfaceConfig = { // 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', // 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', // 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', - // 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' + // 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' // ], TOOLBAR_TIMEOUT: 4000, diff --git a/lang/main.json b/lang/main.json index da250cff43..bbdcf7a400 100644 --- a/lang/main.json +++ b/lang/main.json @@ -332,6 +332,11 @@ "embedMeeting": { "title": "Embed this meeting" }, + "virtualBackground": { + "title": "Backgrounds", + "enableBlur": "Enable blur", + "removeBackground": "Remove background" + }, "feedback": { "average": "Average", "bad": "Bad", @@ -748,7 +753,7 @@ "toggleCamera": "Toggle camera", "toggleFilmstrip": "Toggle filmstrip", "videomute": "Toggle mute video", - "videoblur": "Toggle video blur" + "selectBackground": "Select Background" }, "addPeople": "Add people to your call", "audioSettings": "Audio settings", @@ -810,9 +815,7 @@ "tileViewToggle": "Toggle tile view", "toggleCamera": "Toggle camera", "videomute": "Start / Stop camera", - "videoSettings": "Video settings", - "startvideoblur": "Blur my background", - "stopvideoblur": "Disable background blur" + "selectBackground": "Select background" }, "transcribing": { "ccButtonTooltip": "Start / Stop subtitles", diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js index f337a358e3..e41000c22f 100644 --- a/react/features/app/reducers.any.js +++ b/react/features/app/reducers.any.js @@ -25,7 +25,6 @@ import '../base/testing/reducer'; import '../base/tracks/reducer'; import '../base/user-interaction/reducer'; import '../billing-counter/reducer'; -import '../blur/reducer'; import '../calendar-sync/reducer'; import '../chat/reducer'; import '../deep-linking/reducer'; diff --git a/react/features/app/reducers.web.js b/react/features/app/reducers.web.js index ce08a5aeb0..989b9c9967 100644 --- a/react/features/app/reducers.web.js +++ b/react/features/app/reducers.web.js @@ -12,5 +12,5 @@ import '../remote-control/reducer'; import '../screenshot-capture/reducer'; import '../shared-video/reducer'; import '../talk-while-muted/reducer'; - +import '../virtual-background/reducer'; import './reducers.any'; diff --git a/react/features/base/config/constants.js b/react/features/base/config/constants.js index bebf5aba5d..8f63ecb7ac 100644 --- a/react/features/base/config/constants.js +++ b/react/features/base/config/constants.js @@ -18,5 +18,5 @@ export const TOOLBAR_BUTTONS = [ 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', - 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' + 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' ]; diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js index 9160ce8b92..f5f84e4cdf 100644 --- a/react/features/base/icons/svg/index.js +++ b/react/features/base/icons/svg/index.js @@ -109,6 +109,7 @@ export { default as IconVideoQualityAudioOnly } from './AUD.svg'; export { default as IconVideoQualityHD } from './HD.svg'; export { default as IconVideoQualityLD } from './LD.svg'; export { default as IconVideoQualitySD } from './SD.svg'; +export { default as IconVirtualBackground } from './virtual-background.svg'; export { default as IconVolume } from './volume.svg'; export { default as IconVolumeEmpty } from './volume-empty.svg'; export { default as IconVolumeOff } from './volume-off.svg'; diff --git a/react/features/base/icons/svg/virtual-background.svg b/react/features/base/icons/svg/virtual-background.svg new file mode 100644 index 0000000000..da6b62dec9 --- /dev/null +++ b/react/features/base/icons/svg/virtual-background.svg @@ -0,0 +1,3 @@ + + + diff --git a/react/features/base/tracks/loadEffects.web.js b/react/features/base/tracks/loadEffects.web.js index 8fe8a759bd..b1eda35aa6 100644 --- a/react/features/base/tracks/loadEffects.web.js +++ b/react/features/base/tracks/loadEffects.web.js @@ -1,7 +1,7 @@ // @flow -import { getBlurEffect } from '../../blur'; import { createScreenshotCaptureEffect } from '../../stream-effects/screenshot-capture'; +import { getBackgroundEffect } from '../../virtual-background'; import logger from './logger'; @@ -14,10 +14,10 @@ import logger from './logger'; export default function loadEffects(store: Object): Promise { const state = store.getState(); - const blurPromise = state['features/blur'].blurEnabled - ? getBlurEffect() + const backgroundPromise = state['features/virtual-background'].backgroundEffectEnabled + ? getBackgroundEffect() .catch(error => { - logger.error('Failed to obtain the blur effect instance with error: ', error); + logger.error('Failed to obtain the background effect instance with error: ', error); return Promise.resolve(); }) @@ -31,5 +31,5 @@ export default function loadEffects(store: Object): Promise { }) : Promise.resolve(); - return Promise.all([ blurPromise, screenshotCapturePromise ]); + return Promise.all([ backgroundPromise, screenshotCapturePromise ]); } diff --git a/react/features/blur/actionTypes.js b/react/features/blur/actionTypes.js deleted file mode 100644 index 9f5fc11e5a..0000000000 --- a/react/features/blur/actionTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow - -/** - * The type of redux action dispatched which represents that the blur - * is enabled. - * - * { - * type: BLUR_ENABLED - * } - */ -export const BLUR_ENABLED = 'BLUR_ENABLED'; - -/** - * The type of redux action dispatched which represents that the blur - * is disabled. - * - * { - * type: BLUR_DISABLED - * } - */ -export const BLUR_DISABLED = 'BLUR_DISABLED'; diff --git a/react/features/blur/actions.js b/react/features/blur/actions.js deleted file mode 100644 index 76382ccfa5..0000000000 --- a/react/features/blur/actions.js +++ /dev/null @@ -1,67 +0,0 @@ -// @flow - -import { getLocalVideoTrack } from '../../features/base/tracks'; - -import { BLUR_DISABLED, BLUR_ENABLED } from './actionTypes'; -import { getBlurEffect } from './functions'; -import logger from './logger'; - -/** -* Signals the local participant is switching between blurred or non blurred video. -* -* @param {boolean} enabled - If true enables video blur, false otherwise. -* @returns {Promise} -*/ -export function toggleBlurEffect(enabled: boolean) { - return function(dispatch: (Object) => Object, getState: () => any) { - const state = getState(); - - if (state['features/blur'].blurEnabled !== enabled) { - const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']); - - return getBlurEffect() - .then(blurEffectInstance => - jitsiTrack.setEffect(enabled ? blurEffectInstance : undefined) - .then(() => { - enabled ? dispatch(blurEnabled()) : dispatch(blurDisabled()); - }) - .catch(error => { - enabled ? dispatch(blurDisabled()) : dispatch(blurEnabled()); - logger.error('setEffect failed with error:', error); - }) - ) - .catch(error => { - dispatch(blurDisabled()); - logger.error('getBlurEffect failed with error:', error); - }); - } - - return Promise.resolve(); - }; -} - -/** - * Signals the local participant that the blur has been enabled. - * - * @returns {{ - * type: BLUR_ENABLED - * }} - */ -export function blurEnabled() { - return { - type: BLUR_ENABLED - }; -} - -/** - * Signals the local participant that the blur has been disabled. - * - * @returns {{ - * type: BLUR_DISABLED - * }} - */ -export function blurDisabled() { - return { - type: BLUR_DISABLED - }; -} diff --git a/react/features/blur/components/index.js b/react/features/blur/components/index.js deleted file mode 100644 index 0cdd7d7ed8..0000000000 --- a/react/features/blur/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as VideoBlurButton } from './VideoBlurButton'; diff --git a/react/features/blur/reducer.js b/react/features/blur/reducer.js deleted file mode 100644 index 0ffb92a342..0000000000 --- a/react/features/blur/reducer.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import { ReducerRegistry } from '../base/redux'; - -import { BLUR_ENABLED, BLUR_DISABLED } from './actionTypes'; - - -ReducerRegistry.register('features/blur', (state = {}, action) => { - - switch (action.type) { - case BLUR_ENABLED: { - return { - ...state, - blurEnabled: true - }; - } - case BLUR_DISABLED: { - return { - ...state, - blurEnabled: false - }; - } - } - - return state; -}); diff --git a/react/features/stream-effects/blur/index.js b/react/features/stream-effects/blur/index.js deleted file mode 100644 index c7b56ffe48..0000000000 --- a/react/features/stream-effects/blur/index.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow - -import * as wasmCheck from 'wasm-check'; - -import JitsiStreamBlurEffect from './JitsiStreamBlurEffect'; -import createTFLiteModule from './vendor/tflite/tflite'; -import createTFLiteSIMDModule from './vendor/tflite/tflite-simd'; - -const models = { - 'model96': 'libs/segm_lite_v681.tflite', - 'model144': 'libs/segm_full_v679.tflite' -}; - -const segmentationDimensions = { - 'model96': { - 'height': 96, - 'width': 160 - }, - 'model144': { - 'height': 144, - 'width': 256 - } -}; - -/** - * Creates a new instance of JitsiStreamBlurEffect. This loads the bodyPix model that is used to - * extract person segmentation. - * - * @returns {Promise} - */ -export async function createBlurEffect() { - if (!MediaStreamTrack.prototype.getSettings && !MediaStreamTrack.prototype.getConstraints) { - throw new Error('JitsiStreamBlurEffect not supported!'); - } - let tflite; - - if (wasmCheck.feature.simd) { - tflite = await createTFLiteSIMDModule(); - } else { - tflite = await createTFLiteModule(); - } - - const modelBufferOffset = tflite._getModelBufferMemoryOffset(); - const modelResponse = await fetch( - wasmCheck.feature.simd ? models.model144 : models.model96 - ); - - if (!modelResponse.ok) { - throw new Error('Failed to download tflite model!'); - } - - const model = await modelResponse.arrayBuffer(); - - tflite.HEAPU8.set(new Uint8Array(model), modelBufferOffset); - - tflite._loadModel(model.byteLength); - - const options = wasmCheck.feature.simd ? segmentationDimensions.model144 : segmentationDimensions.model96; - - return new JitsiStreamBlurEffect(tflite, options); -} diff --git a/react/features/stream-effects/blur/JitsiStreamBlurEffect.js b/react/features/stream-effects/virtual-background/JitsiStreamBackgroundEffect.js similarity index 90% rename from react/features/stream-effects/blur/JitsiStreamBlurEffect.js rename to react/features/stream-effects/virtual-background/JitsiStreamBackgroundEffect.js index fc5466e0b5..bf79b209e4 100644 --- a/react/features/stream-effects/blur/JitsiStreamBlurEffect.js +++ b/react/features/stream-effects/virtual-background/JitsiStreamBackgroundEffect.js @@ -9,11 +9,11 @@ import { const blurValue = '25px'; /** - * Represents a modified MediaStream that adds blur to video background. - * JitsiStreamBlurEffect does the processing of the original + * Represents a modified MediaStream that adds effects to video background. + * JitsiStreamBackgroundEffect does the processing of the original * video stream. */ -export default class JitsiStreamBlurEffect { +export default class JitsiStreamBackgroundEffect { _model: Object; _options: Object; _segmentationPixelCount: number; @@ -29,6 +29,7 @@ export default class JitsiStreamBlurEffect { isEnabled: Function; startEffect: Function; stopEffect: Function; + virtualImage: Image; /** * Represents a modified video MediaStream track. @@ -38,6 +39,12 @@ export default class JitsiStreamBlurEffect { * @param {Object} options - Segmentation dimensions. */ constructor(model: Object, options: Object) { + this._options = options; + + if (this._options.virtualBackground.isVirtualBackground) { + this.virtualImage = new Image(); + this.virtualImage.src = this._options.virtualBackground.virtualSource; + } this._model = model; this._options = options; this._segmentationPixelCount = this._options.width * this._options.height; @@ -91,8 +98,12 @@ export default class JitsiStreamBlurEffect { this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0); this._outputCanvasCtx.globalCompositeOperation = 'destination-over'; - this._outputCanvasCtx.filter = `blur(${blurValue})`; - this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0); + if (this._options.virtualBackground.isVirtualBackground) { + this._outputCanvasCtx.drawImage(this.virtualImage, 0, 0); + } else { + this._outputCanvasCtx.filter = `blur(${blurValue})`; + this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0); + } } /** @@ -196,6 +207,7 @@ export default class JitsiStreamBlurEffect { this._segmentationMaskCanvas.width = this._options.width; this._segmentationMaskCanvas.height = this._options.height; this._segmentationMaskCtx = this._segmentationMaskCanvas.getContext('2d'); + this._outputCanvasElement.width = parseInt(width, 10); this._outputCanvasElement.height = parseInt(height, 10); this._outputCanvasCtx = this._outputCanvasElement.getContext('2d'); diff --git a/react/features/stream-effects/blur/TimerWorker.js b/react/features/stream-effects/virtual-background/TimerWorker.js similarity index 100% rename from react/features/stream-effects/blur/TimerWorker.js rename to react/features/stream-effects/virtual-background/TimerWorker.js diff --git a/react/features/stream-effects/virtual-background/index.js b/react/features/stream-effects/virtual-background/index.js new file mode 100644 index 0000000000..f4549f3367 --- /dev/null +++ b/react/features/stream-effects/virtual-background/index.js @@ -0,0 +1,64 @@ +// @flow + +import * as wasmCheck from 'wasm-check'; + +import JitsiStreamBackgroundEffect from './JitsiStreamBackgroundEffect'; +import createTFLiteModule from './vendor/tflite/tflite'; +import createTFLiteSIMDModule from './vendor/tflite/tflite-simd'; + +const models = { + model96: 'libs/segm_lite_v681.tflite', + model144: 'libs/segm_full_v679.tflite' +}; + +const segmentationDimensions = { + model96: { + height: 96, + width: 160 + }, + model144: { + height: 144, + width: 256 + } +}; + +/** + * Creates a new instance of JitsiStreamBackgroundEffect. This loads the Meet background model that is used to + * extract person segmentation. + * + * @param {Object} virtualBackground - The virtual object that contains the background image source and + * the isVirtualBackground flag that indicates if virtual image is activated. + * @returns {Promise} + */ +export async function createVirtualBackgroundEffect(virtualBackground: Object) { + if (!MediaStreamTrack.prototype.getSettings && !MediaStreamTrack.prototype.getConstraints) { + throw new Error('JitsiStreamBackgroundEffect not supported!'); + } + let tflite; + + if (wasmCheck.feature.simd) { + tflite = await createTFLiteSIMDModule(); + } else { + tflite = await createTFLiteModule(); + } + + const modelBufferOffset = tflite._getModelBufferMemoryOffset(); + const modelResponse = await fetch(wasmCheck.feature.simd ? models.model144 : models.model96); + + if (!modelResponse.ok) { + throw new Error('Failed to download tflite model!'); + } + + const model = await modelResponse.arrayBuffer(); + + tflite.HEAPU8.set(new Uint8Array(model), modelBufferOffset); + + tflite._loadModel(model.byteLength); + + const options = { + ...wasmCheck.feature.simd ? segmentationDimensions.model144 : segmentationDimensions.model96, + virtualBackground + }; + + return new JitsiStreamBackgroundEffect(tflite, options); +} diff --git a/react/features/stream-effects/blur/vendor/README.md b/react/features/stream-effects/virtual-background/vendor/README.md similarity index 100% rename from react/features/stream-effects/blur/vendor/README.md rename to react/features/stream-effects/virtual-background/vendor/README.md diff --git a/react/features/stream-effects/blur/vendor/models/segm_full_v679.tflite b/react/features/stream-effects/virtual-background/vendor/models/segm_full_v679.tflite similarity index 100% rename from react/features/stream-effects/blur/vendor/models/segm_full_v679.tflite rename to react/features/stream-effects/virtual-background/vendor/models/segm_full_v679.tflite diff --git a/react/features/stream-effects/blur/vendor/models/segm_lite_v681.tflite b/react/features/stream-effects/virtual-background/vendor/models/segm_lite_v681.tflite similarity index 100% rename from react/features/stream-effects/blur/vendor/models/segm_lite_v681.tflite rename to react/features/stream-effects/virtual-background/vendor/models/segm_lite_v681.tflite diff --git a/react/features/stream-effects/blur/vendor/tflite/tflite-simd.js b/react/features/stream-effects/virtual-background/vendor/tflite/tflite-simd.js similarity index 100% rename from react/features/stream-effects/blur/vendor/tflite/tflite-simd.js rename to react/features/stream-effects/virtual-background/vendor/tflite/tflite-simd.js diff --git a/react/features/stream-effects/blur/vendor/tflite/tflite-simd.wasm b/react/features/stream-effects/virtual-background/vendor/tflite/tflite-simd.wasm similarity index 100% rename from react/features/stream-effects/blur/vendor/tflite/tflite-simd.wasm rename to react/features/stream-effects/virtual-background/vendor/tflite/tflite-simd.wasm diff --git a/react/features/stream-effects/blur/vendor/tflite/tflite.js b/react/features/stream-effects/virtual-background/vendor/tflite/tflite.js similarity index 100% rename from react/features/stream-effects/blur/vendor/tflite/tflite.js rename to react/features/stream-effects/virtual-background/vendor/tflite/tflite.js diff --git a/react/features/stream-effects/blur/vendor/tflite/tflite.wasm b/react/features/stream-effects/virtual-background/vendor/tflite/tflite.wasm similarity index 100% rename from react/features/stream-effects/blur/vendor/tflite/tflite.wasm rename to react/features/stream-effects/virtual-background/vendor/tflite/tflite.wasm diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index aa3f7047df..b685ebe394 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -35,8 +35,6 @@ import { connect } from '../../../base/redux'; import { OverflowMenuItem } from '../../../base/toolbox/components'; import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks'; import { isVpaasMeeting } from '../../../billing-counter/functions'; -import { VideoBlurButton } from '../../../blur'; -import { checkBlurSupport } from '../../../blur/functions'; import { CHAT_SIZE, ChatCounter, toggleChat } from '../../../chat'; import { EmbedMeetingDialog } from '../../../embed-meeting'; import { SharedDocumentButton } from '../../../etherpad'; @@ -68,6 +66,8 @@ import { OverflowMenuVideoQualityItem, VideoQualityDialog } from '../../../video-quality'; +import { VideoBackgroundButton } from '../../../virtual-background'; +import { checkBlurSupport } from '../../../virtual-background/functions'; import { setFullScreen, setOverflowMenuVisible, @@ -1017,9 +1017,9 @@ class Toolbox extends Component { && , - this._shouldShowButton('videobackgroundblur') - && , this._shouldShowButton('stats') diff --git a/react/features/virtual-background/actionTypes.js b/react/features/virtual-background/actionTypes.js new file mode 100644 index 0000000000..c6b4f021e5 --- /dev/null +++ b/react/features/virtual-background/actionTypes.js @@ -0,0 +1,23 @@ +// @flow + +/** + * The type of redux action dispatched which represents that the background + * effect is enabled or not. + * + * @returns {{ + * type: BACKGROUND_ENABLED, + * backgroundEffectEnabled: boolean, + * }} + */ +export const BACKGROUND_ENABLED = 'BACKGROUND_ENABLED'; + +/** + * The type of the action which enables or disables virtual background + * + * @returns {{ + * type: SET_VIRTUAL_BACKGROUND, + * isVirtualBackground: boolean, + * virtualSource: string, + * }} + */ +export const SET_VIRTUAL_BACKGROUND = 'SET_VIRTUAL_BACKGROUND'; diff --git a/react/features/virtual-background/actions.js b/react/features/virtual-background/actions.js new file mode 100644 index 0000000000..6667af468c --- /dev/null +++ b/react/features/virtual-background/actions.js @@ -0,0 +1,70 @@ +// @flow + +import { getLocalVideoTrack } from '../../features/base/tracks'; + +import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes'; +import { getBackgroundEffect } from './functions'; +import logger from './logger'; + +/** + * Signals the local participant activate the virtual background video or not. + * + * @param {boolean} enabled - If true enables video background, false otherwise. + * @returns {Promise} + */ +export function toggleBackgroundEffect(enabled: boolean) { + return async function(dispatch: Object => Object, getState: () => any) { + const state = getState(); + + const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']); + const virtualBackground = state['features/virtual-background']; + + try { + if (enabled) { + await jitsiTrack.setEffect(await getBackgroundEffect(virtualBackground)); + dispatch(backgroundEnabled(true)); + } else { + await jitsiTrack.setEffect(undefined); + dispatch(backgroundEnabled(false)); + } + } catch (error) { + dispatch(backgroundEnabled(false)); + logger.error('Error on apply backgroun effect:', error); + } + }; +} + +/** + * Sets the selected virtual background image object. + * + * @param {Object} virtualSource - Virtual background image source. + * @param {boolean} isVirtualBackground - Indicate if virtual image is activated. + * @returns {{ + * type: SET_VIRTUAL_BACKGROUND, + * virtualSource: string, + * isVirtualBackground: boolean, + * }} + */ +export function setVirtualBackground(virtualSource: string, isVirtualBackground: boolean) { + return { + type: SET_VIRTUAL_BACKGROUND, + virtualSource, + isVirtualBackground + }; +} + +/** + * Signals the local participant that the background effect has been enabled. + * + * @param {boolean} backgroundEffectEnabled - Indicate if virtual background effect is activated. + * @returns {{ + * type: BACKGROUND_ENABLED, + * backgroundEffectEnabled: boolean, + * }} + */ +export function backgroundEnabled(backgroundEffectEnabled: boolean) { + return { + type: BACKGROUND_ENABLED, + backgroundEffectEnabled + }; +} diff --git a/react/features/blur/components/VideoBlurButton.js b/react/features/virtual-background/components/VideoBackgroundButton.js similarity index 58% rename from react/features/blur/components/VideoBlurButton.js rename to react/features/virtual-background/components/VideoBackgroundButton.js index acb223df6d..d004fecbc1 100644 --- a/react/features/blur/components/VideoBlurButton.js +++ b/react/features/virtual-background/components/VideoBackgroundButton.js @@ -1,23 +1,24 @@ // @flow -import { createVideoBlurEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; -import { IconBlurBackground } from '../../base/icons'; +import { IconVirtualBackground } from '../../base/icons'; import { connect } from '../../base/redux'; import { AbstractButton } from '../../base/toolbox/components'; import type { AbstractButtonProps } from '../../base/toolbox/components'; import { isLocalCameraTrackMuted } from '../../base/tracks'; -import { toggleBlurEffect } from '../actions'; + +import { VirtualBackgroundDialog } from './index'; /** - * The type of the React {@code Component} props of {@link VideoBlurButton}. + * The type of the React {@code Component} props of {@link VideoBackgroundButton}. */ type Props = AbstractButtonProps & { /** * True if the video background is blurred or false if it is not. */ - _isVideoBlurred: boolean, + _isBackgroundEnabled: boolean, /** * Whether video is currently muted or not. @@ -28,42 +29,39 @@ type Props = AbstractButtonProps & { * The redux {@code dispatch} function. */ dispatch: Function - }; /** - * An abstract implementation of a button that toggles the video blur effect. + * An abstract implementation of a button that toggles the video background dialog. */ -class VideoBlurButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.videoblur'; - icon = IconBlurBackground; - label = 'toolbar.startvideoblur'; - toggledLabel = 'toolbar.stopvideoblur'; +class VideoBackgroundButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.selectBackground'; + icon = IconVirtualBackground; + label = 'toolbar.selectBackground'; + tooltip = 'toolbar.selectBackground'; /** - * Handles clicking / pressing the button, and toggles the blur effect + * Handles clicking / pressing the button, and toggles the virtual background dialog * state accordingly. * * @protected * @returns {void} */ _handleClick() { - const { _isVideoBlurred, dispatch } = this.props; - const value = !_isVideoBlurred; + const { dispatch } = this.props; - sendAnalytics(createVideoBlurEvent(value ? 'started' : 'stopped')); - dispatch(toggleBlurEffect(value)); + dispatch(openDialog(VirtualBackgroundDialog)); } /** - * Returns {@code boolean} value indicating if the blur effect is + * Returns {@code boolean} value indicating if the background effect is * enabled or not. * * @protected * @returns {boolean} */ _isToggled() { - return this.props._isVideoBlurred; + return this.props._isBackgroundEnabled; } /** @@ -80,22 +78,21 @@ class VideoBlurButton extends AbstractButton { /** * Maps (parts of) the redux state to the associated props for the - * {@code VideoBlurButton} component. + * {@code VideoBackgroundButton} component. * * @param {Object} state - The Redux state. * @private * @returns {{ - * _isVideoBlurred: boolean + * _isBackgroundEnabled: boolean * }} */ function _mapStateToProps(state): Object { const tracks = state['features/base/tracks']; return { - _isVideoBlurred: Boolean(state['features/blur'].blurEnabled), + _isBackgroundEnabled: Boolean(state['features/virtual-background'].backgroundEffectEnabled), _videoMuted: isLocalCameraTrackMuted(tracks) }; } -export default translate(connect(_mapStateToProps)(VideoBlurButton)); - +export default translate(connect(_mapStateToProps)(VideoBackgroundButton)); diff --git a/react/features/virtual-background/components/VirtualBackgroundDialog.js b/react/features/virtual-background/components/VirtualBackgroundDialog.js new file mode 100644 index 0000000000..15fee4000d --- /dev/null +++ b/react/features/virtual-background/components/VirtualBackgroundDialog.js @@ -0,0 +1,118 @@ +// @flow +/* eslint-disable react/jsx-no-bind, no-return-assign */ +import React, { useState } from 'react'; + +import { Dialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { Icon, IconBlurBackground } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { Tooltip } from '../../base/tooltip'; +import { toggleBackgroundEffect, setVirtualBackground } from '../actions'; + +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' + } +]; +type Props = { + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function, + + /** + * Invoked to obtain translated strings. + */ + t: Function +}; + +/** + * Renders virtual background dialog. + * + * @returns {ReactElement} + */ +function VirtualBackground({ dispatch, t }: Props) { + const [ selected, setSelected ] = useState(''); + const enableBlur = () => { + setSelected('blur'); + dispatch(setVirtualBackground('', false)); + dispatch(toggleBackgroundEffect(true)); + }; + + const removeBackground = () => { + setSelected('none'); + dispatch(setVirtualBackground('', false)); + dispatch(toggleBackgroundEffect(false)); + }; + + const addImageBackground = image => { + setSelected(image.id); + dispatch(setVirtualBackground(image.src, true)); + dispatch(toggleBackgroundEffect(true)); + }; + + return ( + +
+ +
removeBackground() }> + None +
+
+ + enableBlur() } + size = { 50 } + src = { IconBlurBackground } /> + + {images.map((image, index) => ( + + addImageBackground(image) } + onError = { event => event.target.style.display = 'none' } + src = { image.src } /> + + ))} +
+
+ ); +} + +export default translate(connect()(VirtualBackground)); diff --git a/react/features/virtual-background/components/index.js b/react/features/virtual-background/components/index.js new file mode 100644 index 0000000000..2a31b4ab94 --- /dev/null +++ b/react/features/virtual-background/components/index.js @@ -0,0 +1,2 @@ +export { default as VideoBackgroundButton } from './VideoBackgroundButton'; +export { default as VirtualBackgroundDialog } from './VirtualBackgroundDialog'; diff --git a/react/features/blur/functions.js b/react/features/virtual-background/functions.js similarity index 51% rename from react/features/blur/functions.js rename to react/features/virtual-background/functions.js index c566acbf3f..4ef464bb33 100644 --- a/react/features/blur/functions.js +++ b/react/features/virtual-background/functions.js @@ -7,16 +7,19 @@ let filterSupport; /** * Returns promise that resolves with the blur effect instance. * - * @returns {Promise} - Resolves with the blur effect instance. + * @param {Object} virtualBackground - The virtual object that contains the background image source and + * the isVirtualBackground flag that indicates if virtual image is activated . + * @returns {Promise} - Resolves with the background effect instance. */ -export function getBlurEffect() { +export function getBackgroundEffect(virtualBackground: Object) { const ns = getJitsiMeetGlobalNS(); - if (ns.effects && ns.effects.createBlurEffect) { - return ns.effects.createBlurEffect(); + if (ns.effects && ns.effects.createVirtualBackgroundEffect) { + return ns.effects.createVirtualBackgroundEffect(virtualBackground); } - return loadScript('libs/video-blur-effect.min.js').then(() => ns.effects.createBlurEffect()); + return loadScript('libs/virtual-background-effect.min.js').then(() => + ns.effects.createVirtualBackgroundEffect(virtualBackground)); } /** diff --git a/react/features/blur/index.js b/react/features/virtual-background/index.js similarity index 100% rename from react/features/blur/index.js rename to react/features/virtual-background/index.js diff --git a/react/features/blur/logger.js b/react/features/virtual-background/logger.js similarity index 53% rename from react/features/blur/logger.js rename to react/features/virtual-background/logger.js index f83fb56583..a4322f9053 100644 --- a/react/features/blur/logger.js +++ b/react/features/virtual-background/logger.js @@ -2,4 +2,4 @@ import { getLogger } from '../base/logging/functions'; -export default getLogger('features/blur'); +export default getLogger('features/virtual-background'); diff --git a/react/features/virtual-background/reducer.js b/react/features/virtual-background/reducer.js new file mode 100644 index 0000000000..7357fe451d --- /dev/null +++ b/react/features/virtual-background/reducer.js @@ -0,0 +1,38 @@ +// @flow + +import { ReducerRegistry } from '../base/redux'; + +import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes'; + +/** + * Reduces redux actions which activate/deactivate virtual background image, or + * indicate if the virtual image background is activated/deactivated. The + * backgroundEffectEnabled flag indicate if virtual background effect is activated. + * + * @param {State} state - The current redux state. + * @param {Action} action - The redux action to reduce. + * @param {string} action.type - The type of the redux action to reduce.. + * @returns {State} The next redux state that is the result of reducing the + * specified action. + */ +ReducerRegistry.register('features/virtual-background', (state = {}, action) => { + const { virtualSource, isVirtualBackground, backgroundEffectEnabled } = action; + + switch (action.type) { + case SET_VIRTUAL_BACKGROUND: { + return { + ...state, + virtualSource, + isVirtualBackground + }; + } + case BACKGROUND_ENABLED: { + return { + ...state, + backgroundEffectEnabled + }; + } + } + + return state; +}); diff --git a/webpack.config.js b/webpack.config.js index deae5c39b0..ede3054bf0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -240,7 +240,7 @@ module.exports = [ performance: getPerformanceHints(128 * 1024) }), - // Because both video-blur-effect and rnnoise-processor modules are loaded + // Because both virtual-background-effect and rnnoise-processor modules are loaded // in a lazy manner using the loadScript function with a hard coded name, // i.e.loadScript('libs/rnnoise-processor.min.js'), webpack dev server // won't know how to properly load them using the default config filename @@ -249,7 +249,7 @@ module.exports = [ // prod and dev mode. Object.assign({}, config, { entry: { - 'video-blur-effect': './react/features/stream-effects/blur/index.js' + 'virtual-background-effect': './react/features/stream-effects/virtual-background/index.js' }, output: Object.assign({}, config.output, { library: [ 'JitsiMeetJS', 'app', 'effects' ],