Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

VirtualBackgroundDialog.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. // @flow
  2. import Spinner from '@atlaskit/spinner';
  3. import Bourne from '@hapi/bourne';
  4. import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
  5. import React, { useState, useEffect, useCallback } from 'react';
  6. import { useSelector } from 'react-redux';
  7. import { Dialog, hideDialog, openDialog } from '../../base/dialog';
  8. import { translate } from '../../base/i18n';
  9. import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
  10. import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
  11. import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
  12. import { VIDEO_TYPE } from '../../base/media';
  13. import { connect } from '../../base/redux';
  14. import { updateSettings } from '../../base/settings';
  15. import { Tooltip } from '../../base/tooltip';
  16. import { getLocalVideoTrack } from '../../base/tracks';
  17. import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification } from '../../notifications';
  18. import { toggleBackgroundEffect, virtualBackgroundTrackChanged } from '../actions';
  19. import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
  20. import { toDataURL } from '../functions';
  21. import logger from '../logger';
  22. import UploadImageButton from './UploadImageButton';
  23. import VirtualBackgroundPreview from './VirtualBackgroundPreview';
  24. type Props = {
  25. /**
  26. * The list of Images to choose from.
  27. */
  28. _images: Array<Image>,
  29. /**
  30. * The current local flip x status.
  31. */
  32. _localFlipX: boolean,
  33. /**
  34. * Returns the jitsi track that will have backgraund effect applied.
  35. */
  36. _jitsiTrack: Object,
  37. /**
  38. * Returns the selected thumbnail identifier.
  39. */
  40. _selectedThumbnail: string,
  41. /**
  42. * If the upload button should be displayed or not.
  43. */
  44. _showUploadButton: boolean,
  45. /**
  46. * Returns the selected virtual background object.
  47. */
  48. _virtualBackground: Object,
  49. /**
  50. * The redux {@code dispatch} function.
  51. */
  52. dispatch: Function,
  53. /**
  54. * The initial options copied in the state for the {@code VirtualBackground} component.
  55. *
  56. * NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing
  57. * selection.
  58. */
  59. initialOptions: Object,
  60. /**
  61. * Invoked to obtain translated strings.
  62. */
  63. t: Function
  64. };
  65. const onError = event => {
  66. event.target.style.display = 'none';
  67. };
  68. /**
  69. * Maps (parts of) the redux state to the associated props for the
  70. * {@code VirtualBackground} component.
  71. *
  72. * @param {Object} state - The Redux state.
  73. * @private
  74. * @returns {{Props}}
  75. */
  76. function _mapStateToProps(state): Object {
  77. const { localFlipX } = state['features/base/settings'];
  78. const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
  79. const hasBrandingImages = Boolean(dynamicBrandingImages.length);
  80. return {
  81. _localFlipX: Boolean(localFlipX),
  82. _images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
  83. _virtualBackground: state['features/virtual-background'],
  84. _selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
  85. _showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
  86. _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
  87. };
  88. }
  89. const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
  90. /**
  91. * Renders virtual background dialog.
  92. *
  93. * @returns {ReactElement}
  94. */
  95. function VirtualBackground({
  96. _images,
  97. _jitsiTrack,
  98. _localFlipX,
  99. _selectedThumbnail,
  100. _showUploadButton,
  101. _virtualBackground,
  102. dispatch,
  103. initialOptions,
  104. t
  105. }: Props) {
  106. const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
  107. const [ options, setOptions ] = useState({ ...initialOptions });
  108. const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
  109. const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
  110. const [ loading, setLoading ] = useState(false);
  111. const { disableScreensharingVirtualBackground } = useSelector(state => state['features/base/config']);
  112. const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
  113. ? _virtualBackground.virtualSource
  114. : null);
  115. const [ initialVirtualBackground ] = useState(_virtualBackground);
  116. const deleteStoredImage = useCallback(e => {
  117. const imageId = e.currentTarget.getAttribute('data-imageid');
  118. setStoredImages(storedImages.filter(item => item.id !== imageId));
  119. }, [ storedImages ]);
  120. const deleteStoredImageKeyPress = useCallback(e => {
  121. if (e.key === ' ' || e.key === 'Enter') {
  122. e.preventDefault();
  123. deleteStoredImage(e);
  124. }
  125. }, [ deleteStoredImage ]);
  126. /**
  127. * Updates stored images on local storage.
  128. */
  129. useEffect(() => {
  130. try {
  131. jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
  132. } catch (err) {
  133. // Preventing localStorage QUOTA_EXCEEDED_ERR
  134. err && setStoredImages(storedImages.slice(1));
  135. }
  136. if (storedImages.length === BACKGROUNDS_LIMIT) {
  137. setStoredImages(storedImages.slice(1));
  138. }
  139. }, [ storedImages ]);
  140. const enableBlur = useCallback(async () => {
  141. setOptions({
  142. backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
  143. enabled: true,
  144. blurValue: 25,
  145. selectedThumbnail: 'blur'
  146. });
  147. logger.info('"Blur" option setted for virtual background preview!');
  148. }, []);
  149. const enableBlurKeyPress = useCallback(e => {
  150. if (e.key === ' ' || e.key === 'Enter') {
  151. e.preventDefault();
  152. enableBlur();
  153. }
  154. }, [ enableBlur ]);
  155. const enableSlideBlur = useCallback(async () => {
  156. setOptions({
  157. backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
  158. enabled: true,
  159. blurValue: 8,
  160. selectedThumbnail: 'slight-blur'
  161. });
  162. logger.info('"Slight-blur" option setted for virtual background preview!');
  163. }, []);
  164. const enableSlideBlurKeyPress = useCallback(e => {
  165. if (e.key === ' ' || e.key === 'Enter') {
  166. e.preventDefault();
  167. enableSlideBlur();
  168. }
  169. }, [ enableSlideBlur ]);
  170. const shareDesktop = useCallback(async () => {
  171. if (disableScreensharingVirtualBackground) {
  172. return;
  173. }
  174. let isCancelled = false, url;
  175. try {
  176. url = await createLocalTrack('desktop', '');
  177. } catch (e) {
  178. if (e.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
  179. isCancelled = true;
  180. } else {
  181. logger.error(e);
  182. }
  183. }
  184. if (!url) {
  185. if (!isCancelled) {
  186. dispatch(showErrorNotification({
  187. titleKey: 'virtualBackground.desktopShareError'
  188. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  189. logger.error('Could not create desktop share as a virtual background!');
  190. }
  191. /**
  192. * For electron createLocalTrack will open the {@code DesktopPicker} dialog and hide the
  193. * {@code VirtualBackgroundDialog}. That's why we need to reopen the {@code VirtualBackgroundDialog}
  194. * and restore the current state through {@code initialOptions} prop.
  195. */
  196. if (browser.isElectron()) {
  197. dispatch(openDialog(VirtualBackgroundDialog, { initialOptions: options }));
  198. }
  199. return;
  200. }
  201. const newOptions = {
  202. backgroundType: VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE,
  203. enabled: true,
  204. selectedThumbnail: 'desktop-share',
  205. url
  206. };
  207. /**
  208. * For electron createLocalTrack will open the {@code DesktopPicker} dialog and hide the
  209. * {@code VirtualBackgroundDialog}. That's why we need to reopen the {@code VirtualBackgroundDialog}
  210. * and force it to show desktop share virtual background through {@code initialOptions} prop.
  211. */
  212. if (browser.isElectron()) {
  213. dispatch(openDialog(VirtualBackgroundDialog, { initialOptions: newOptions }));
  214. } else {
  215. setOptions(newOptions);
  216. logger.info('"Desktop-share" option setted for virtual background preview!');
  217. }
  218. }, [ dispatch, options ]);
  219. const shareDesktopKeyPress = useCallback(e => {
  220. if (e.key === ' ' || e.key === 'Enter') {
  221. e.preventDefault();
  222. shareDesktop();
  223. }
  224. }, [ shareDesktop ]);
  225. const removeBackground = useCallback(async () => {
  226. setOptions({
  227. enabled: false,
  228. selectedThumbnail: 'none'
  229. });
  230. logger.info('"None" option setted for virtual background preview!');
  231. }, []);
  232. const removeBackgroundKeyPress = useCallback(e => {
  233. if (e.key === ' ' || e.key === 'Enter') {
  234. e.preventDefault();
  235. removeBackground();
  236. }
  237. }, [ removeBackground ]);
  238. const setUploadedImageBackground = useCallback(async e => {
  239. const imageId = e.currentTarget.getAttribute('data-imageid');
  240. const image = storedImages.find(img => img.id === imageId);
  241. if (image) {
  242. setOptions({
  243. backgroundType: 'image',
  244. enabled: true,
  245. url: image.src,
  246. selectedThumbnail: image.id
  247. });
  248. logger.info('Uploaded image setted for virtual background preview!');
  249. }
  250. }, [ storedImages ]);
  251. const setImageBackground = useCallback(async e => {
  252. const imageId = e.currentTarget.getAttribute('data-imageid');
  253. const image = _images.find(img => img.id === imageId);
  254. if (image) {
  255. try {
  256. const url = await toDataURL(image.src);
  257. setOptions({
  258. backgroundType: 'image',
  259. enabled: true,
  260. url,
  261. selectedThumbnail: image.id
  262. });
  263. logger.info('Image set for virtual background preview!');
  264. } catch (err) {
  265. logger.error('Could not fetch virtual background image:', err);
  266. }
  267. setLoading(false);
  268. }
  269. }, []);
  270. const setImageBackgroundKeyPress = useCallback(e => {
  271. if (e.key === ' ' || e.key === 'Enter') {
  272. e.preventDefault();
  273. setImageBackground(e);
  274. }
  275. }, [ setImageBackground ]);
  276. const setUploadedImageBackgroundKeyPress = useCallback(e => {
  277. if (e.key === ' ' || e.key === 'Enter') {
  278. e.preventDefault();
  279. setUploadedImageBackground(e);
  280. }
  281. }, [ setUploadedImageBackground ]);
  282. const applyVirtualBackground = useCallback(async () => {
  283. if (activeDesktopVideo) {
  284. await activeDesktopVideo.dispose();
  285. }
  286. setLoading(true);
  287. await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
  288. await setLoading(false);
  289. if (_localFlipX && options.backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE) {
  290. dispatch(updateSettings({
  291. localFlipX: !_localFlipX
  292. }));
  293. } else {
  294. // Set x scale to default value.
  295. dispatch(updateSettings({
  296. localFlipX: true
  297. }));
  298. }
  299. dispatch(hideDialog());
  300. logger.info(`Virtual background type: '${typeof options.backgroundType === 'undefined'
  301. ? 'none' : options.backgroundType}' applied!`);
  302. dispatch(virtualBackgroundTrackChanged());
  303. }, [ dispatch, options, _localFlipX ]);
  304. // Prevent the selection of a new virtual background if it has not been applied by default
  305. const cancelVirtualBackground = useCallback(async () => {
  306. await setOptions({
  307. backgroundType: initialVirtualBackground.backgroundType,
  308. enabled: initialVirtualBackground.backgroundEffectEnabled,
  309. url: initialVirtualBackground.virtualSource,
  310. selectedThumbnail: initialVirtualBackground.selectedThumbnail,
  311. blurValue: initialVirtualBackground.blurValue
  312. });
  313. dispatch(hideDialog());
  314. });
  315. const loadedPreviewState = useCallback(async loaded => {
  316. await setPreviewIsLoaded(loaded);
  317. });
  318. return (
  319. <Dialog
  320. hideCancelButton = { false }
  321. okKey = { 'virtualBackground.apply' }
  322. onCancel = { cancelVirtualBackground }
  323. onSubmit = { applyVirtualBackground }
  324. submitDisabled = { !options || loading || !previewIsLoaded }
  325. titleKey = { 'virtualBackground.title' } >
  326. <VirtualBackgroundPreview
  327. loadedPreview = { loadedPreviewState }
  328. options = { options } />
  329. {loading ? (
  330. <div className = 'virtual-background-loading'>
  331. <Spinner
  332. isCompleting = { false }
  333. size = 'medium' />
  334. </div>
  335. ) : (
  336. <div>
  337. {_showUploadButton
  338. && <UploadImageButton
  339. setLoading = { setLoading }
  340. setOptions = { setOptions }
  341. setStoredImages = { setStoredImages }
  342. showLabel = { previewIsLoaded }
  343. storedImages = { storedImages } />}
  344. <div
  345. className = 'virtual-background-dialog'
  346. role = 'radiogroup'
  347. tabIndex = '-1'>
  348. <Tooltip
  349. content = { t('virtualBackground.removeBackground') }
  350. position = { 'top' }>
  351. <div
  352. aria-checked = { _selectedThumbnail === 'none' }
  353. aria-label = { t('virtualBackground.removeBackground') }
  354. className = { _selectedThumbnail === 'none' ? 'background-option none-selected'
  355. : 'background-option virtual-background-none' }
  356. onClick = { removeBackground }
  357. onKeyPress = { removeBackgroundKeyPress }
  358. role = 'radio'
  359. tabIndex = { 0 } >
  360. {t('virtualBackground.none')}
  361. </div>
  362. </Tooltip>
  363. <Tooltip
  364. content = { t('virtualBackground.slightBlur') }
  365. position = { 'top' }>
  366. <div
  367. aria-checked = { _selectedThumbnail === 'slight-blur' }
  368. aria-label = { t('virtualBackground.slightBlur') }
  369. className = { _selectedThumbnail === 'slight-blur'
  370. ? 'background-option slight-blur-selected' : 'background-option slight-blur' }
  371. onClick = { enableSlideBlur }
  372. onKeyPress = { enableSlideBlurKeyPress }
  373. role = 'radio'
  374. tabIndex = { 0 }>
  375. {t('virtualBackground.slightBlur')}
  376. </div>
  377. </Tooltip>
  378. <Tooltip
  379. content = { t('virtualBackground.blur') }
  380. position = { 'top' }>
  381. <div
  382. aria-checked = { _selectedThumbnail === 'blur' }
  383. aria-label = { t('virtualBackground.blur') }
  384. className = { _selectedThumbnail === 'blur' ? 'background-option blur-selected'
  385. : 'background-option blur' }
  386. onClick = { enableBlur }
  387. onKeyPress = { enableBlurKeyPress }
  388. role = 'radio'
  389. tabIndex = { 0 }>
  390. {t('virtualBackground.blur')}
  391. </div>
  392. </Tooltip>
  393. {!disableScreensharingVirtualBackground && (
  394. <Tooltip
  395. content = { t('virtualBackground.desktopShare') }
  396. position = { 'top' }>
  397. <div
  398. aria-checked = { _selectedThumbnail === 'desktop-share' }
  399. aria-label = { t('virtualBackground.desktopShare') }
  400. className = { _selectedThumbnail === 'desktop-share'
  401. ? 'background-option desktop-share-selected'
  402. : 'background-option desktop-share' }
  403. onClick = { shareDesktop }
  404. onKeyPress = { shareDesktopKeyPress }
  405. role = 'radio'
  406. tabIndex = { 0 }>
  407. <Icon
  408. className = 'share-desktop-icon'
  409. size = { 30 }
  410. src = { IconShareDesktop } />
  411. </div>
  412. </Tooltip>
  413. )}
  414. {_images.map(image => (
  415. <Tooltip
  416. content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
  417. key = { image.id }
  418. position = { 'top' }>
  419. <img
  420. alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
  421. aria-checked = { options.selectedThumbnail === image.id
  422. || _selectedThumbnail === image.id }
  423. className = {
  424. options.selectedThumbnail === image.id || _selectedThumbnail === image.id
  425. ? 'background-option thumbnail-selected' : 'background-option thumbnail' }
  426. data-imageid = { image.id }
  427. onClick = { setImageBackground }
  428. onError = { onError }
  429. onKeyPress = { setImageBackgroundKeyPress }
  430. role = 'radio'
  431. src = { image.src }
  432. tabIndex = { 0 } />
  433. </Tooltip>
  434. ))}
  435. {storedImages.map((image, index) => (
  436. <div
  437. className = { 'thumbnail-container' }
  438. key = { image.id }>
  439. <img
  440. alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
  441. aria-checked = { _selectedThumbnail === image.id }
  442. className = { _selectedThumbnail === image.id
  443. ? 'background-option thumbnail-selected' : 'background-option thumbnail' }
  444. data-imageid = { image.id }
  445. onClick = { setUploadedImageBackground }
  446. onError = { onError }
  447. onKeyPress = { setUploadedImageBackgroundKeyPress }
  448. role = 'radio'
  449. src = { image.src }
  450. tabIndex = { 0 } />
  451. <Icon
  452. ariaLabel = { t('virtualBackground.deleteImage') }
  453. className = { 'delete-image-icon' }
  454. data-imageid = { image.id }
  455. onClick = { deleteStoredImage }
  456. onKeyPress = { deleteStoredImageKeyPress }
  457. role = 'button'
  458. size = { 15 }
  459. src = { IconCloseSmall }
  460. tabIndex = { 0 } />
  461. </div>
  462. ))}
  463. </div>
  464. </div>
  465. )}
  466. </Dialog>
  467. );
  468. }
  469. export default VirtualBackgroundDialog;