You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VirtualBackgroundPreview.tsx 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /* eslint-disable lines-around-comment */
  2. import Spinner from '@atlaskit/spinner';
  3. import { Theme } from '@mui/material';
  4. import { withStyles } from '@mui/styles';
  5. import React, { PureComponent } from 'react';
  6. import { WithTranslation } from 'react-i18next';
  7. import { IState } from '../../app/types';
  8. // @ts-ignore
  9. import { hideDialog } from '../../base/dialog';
  10. // @ts-ignore
  11. import { translate } from '../../base/i18n';
  12. // @ts-ignore
  13. import Video from '../../base/media/components/Video';
  14. import { VIDEO_TYPE } from '../../base/media/constants';
  15. import { connect, equals } from '../../base/redux/functions';
  16. // @ts-ignore
  17. import { getCurrentCameraDeviceId } from '../../base/settings';
  18. // @ts-ignore
  19. import { createLocalTracksF } from '../../base/tracks/functions';
  20. // @ts-ignore
  21. import { showWarningNotification } from '../../notifications/actions';
  22. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  23. // @ts-ignore
  24. import { toggleBackgroundEffect } from '../actions';
  25. import { VIRTUAL_BACKGROUND_TYPE } from '../constants';
  26. // @ts-ignore
  27. import { localTrackStopped } from '../functions';
  28. // @ts-ignore
  29. import logger from '../logger';
  30. const videoClassName = 'video-preview-video';
  31. /**
  32. * The type of the React {@code PureComponent} props of {@link VirtualBackgroundPreview}.
  33. */
  34. export type Props = WithTranslation & {
  35. /**
  36. * The deviceId of the camera device currently being used.
  37. */
  38. _currentCameraDeviceId: string;
  39. /**
  40. * An object containing the CSS classes.
  41. */
  42. classes: any;
  43. /**
  44. * The redux {@code dispatch} function.
  45. */
  46. dispatch: Function;
  47. /**
  48. * Dialog callback that indicates if the background preview was loaded.
  49. */
  50. loadedPreview: Function;
  51. /**
  52. * Represents the virtual background set options.
  53. */
  54. options: any;
  55. };
  56. /**
  57. * The type of the React {@code Component} state of {@link VirtualBackgroundPreview}.
  58. */
  59. type State = {
  60. /**
  61. * Activate the selected device camera only.
  62. */
  63. jitsiTrack: Object | null;
  64. /**
  65. * Loader activated on setting virtual background.
  66. */
  67. loading: boolean;
  68. /**
  69. * Flag that indicates if the local track was loaded.
  70. */
  71. localTrackLoaded: boolean;
  72. };
  73. /**
  74. * Creates the styles for the component.
  75. *
  76. * @param {Object} theme - The current UI theme.
  77. *
  78. * @returns {Object}
  79. */
  80. const styles = (theme: Theme) => {
  81. return {
  82. virtualBackgroundPreview: {
  83. '& .video-preview': {
  84. height: '250px'
  85. },
  86. '& .video-background-preview-entry': {
  87. marginLeft: '-10px',
  88. height: '250px',
  89. width: '570px',
  90. marginBottom: theme.spacing(2),
  91. zIndex: 2,
  92. '@media (max-width: 632px)': {
  93. maxWidth: '336px'
  94. }
  95. },
  96. '& .video-preview-loader': {
  97. borderRadius: '6px',
  98. backgroundColor: 'transparent',
  99. height: '250px',
  100. marginBottom: theme.spacing(2),
  101. width: '572px',
  102. position: 'fixed',
  103. zIndex: 2,
  104. '& svg': {
  105. position: 'absolute',
  106. top: '40%',
  107. left: '45%'
  108. },
  109. '@media (min-width: 432px) and (max-width: 632px)': {
  110. width: '340px'
  111. }
  112. }
  113. }
  114. };
  115. };
  116. /**
  117. * Implements a React {@link PureComponent} which displays the virtual
  118. * background preview.
  119. *
  120. * @augments PureComponent
  121. */
  122. class VirtualBackgroundPreview extends PureComponent<Props, State> {
  123. _componentWasUnmounted: boolean;
  124. /**
  125. * Initializes a new {@code VirtualBackgroundPreview} instance.
  126. *
  127. * @param {Object} props - The read-only properties with which the new
  128. * instance is to be initialized.
  129. */
  130. constructor(props: Props) {
  131. super(props);
  132. this.state = {
  133. loading: false,
  134. localTrackLoaded: false,
  135. jitsiTrack: null
  136. };
  137. }
  138. /**
  139. * Destroys the jitsiTrack object.
  140. *
  141. * @param {Object} jitsiTrack - The track that needs to be disposed.
  142. * @returns {Promise<void>}
  143. */
  144. _stopStream(jitsiTrack: any) {
  145. if (jitsiTrack) {
  146. jitsiTrack.dispose();
  147. }
  148. }
  149. /**
  150. * Creates and updates the track data.
  151. *
  152. * @returns {void}
  153. */
  154. async _setTracks() {
  155. try {
  156. this.setState({ loading: true });
  157. const [ jitsiTrack ] = await createLocalTracksF({
  158. cameraDeviceId: this.props._currentCameraDeviceId,
  159. devices: [ 'video' ]
  160. });
  161. this.setState({ localTrackLoaded: true });
  162. // In case the component gets unmounted before the tracks are created
  163. // avoid a leak by not setting the state
  164. if (this._componentWasUnmounted) {
  165. this._stopStream(jitsiTrack);
  166. return;
  167. }
  168. this.setState({
  169. jitsiTrack,
  170. loading: false
  171. });
  172. this.props.loadedPreview(true);
  173. } catch (error) {
  174. this.props.dispatch(hideDialog());
  175. this.props.dispatch(
  176. showWarningNotification({
  177. titleKey: 'virtualBackground.backgroundEffectError',
  178. description: 'Failed to access camera device.'
  179. }, NOTIFICATION_TIMEOUT_TYPE.LONG)
  180. );
  181. logger.error('Failed to access camera device. Error on apply background effect.');
  182. return;
  183. }
  184. if (this.props.options.backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE
  185. && this.state.localTrackLoaded) {
  186. this._applyBackgroundEffect();
  187. }
  188. }
  189. /**
  190. * Apply background effect on video preview.
  191. *
  192. * @returns {Promise}
  193. */
  194. async _applyBackgroundEffect() {
  195. this.setState({ loading: true });
  196. this.props.loadedPreview(false);
  197. await this.props.dispatch(toggleBackgroundEffect(this.props.options, this.state.jitsiTrack));
  198. this.props.loadedPreview(true);
  199. this.setState({ loading: false });
  200. }
  201. /**
  202. * Apply video preview loader.
  203. *
  204. * @returns {Promise}
  205. */
  206. _loadVideoPreview() {
  207. return (
  208. <div className = 'video-preview-loader'>
  209. <Spinner
  210. // @ts-ignore
  211. invertColor = { true }
  212. isCompleting = { false }
  213. size = { 'large' } />
  214. </div>
  215. );
  216. }
  217. /**
  218. * Renders a preview entry.
  219. *
  220. * @param {Object} data - The track data.
  221. * @returns {React$Node}
  222. */
  223. _renderPreviewEntry(data: Object) {
  224. const { t } = this.props;
  225. const className = 'video-background-preview-entry';
  226. if (this.state.loading) {
  227. return this._loadVideoPreview();
  228. }
  229. if (!data) {
  230. return (
  231. <div
  232. className = { className }
  233. video-preview-container = { true }>
  234. <div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
  235. </div>
  236. );
  237. }
  238. const props: Object = {
  239. className
  240. };
  241. return (
  242. <div { ...props }>
  243. <Video
  244. className = { videoClassName }
  245. playsinline = { true }
  246. videoTrack = {{ jitsiTrack: data }} />
  247. </div>
  248. );
  249. }
  250. /**
  251. * Implements React's {@link Component#componentDidMount}.
  252. *
  253. * @inheritdoc
  254. */
  255. componentDidMount() {
  256. this._setTracks();
  257. }
  258. /**
  259. * Implements React's {@link Component#componentWillUnmount}.
  260. *
  261. * @inheritdoc
  262. */
  263. componentWillUnmount() {
  264. this._componentWasUnmounted = true;
  265. this._stopStream(this.state.jitsiTrack);
  266. }
  267. /**
  268. * Implements React's {@link Component#componentDidUpdate}.
  269. *
  270. * @inheritdoc
  271. */
  272. async componentDidUpdate(prevProps: Props) {
  273. if (!equals(this.props._currentCameraDeviceId, prevProps._currentCameraDeviceId)) {
  274. this._setTracks();
  275. }
  276. if (!equals(this.props.options, prevProps.options) && this.state.localTrackLoaded) {
  277. if (prevProps.options.backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE) {
  278. prevProps.options.url.dispose();
  279. }
  280. this._applyBackgroundEffect();
  281. }
  282. if (this.props.options.url?.videoType === VIDEO_TYPE.DESKTOP) {
  283. localTrackStopped(this.props.dispatch, this.props.options.url, this.state.jitsiTrack);
  284. }
  285. }
  286. /**
  287. * Implements React's {@link Component#render}.
  288. *
  289. * @inheritdoc
  290. */
  291. render() {
  292. const { jitsiTrack } = this.state;
  293. const { classes } = this.props;
  294. return (<div className = { classes.virtualBackgroundPreview }>
  295. {jitsiTrack
  296. ? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
  297. : <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
  298. }</div>);
  299. }
  300. }
  301. /**
  302. * Maps (parts of) the redux state to the associated props for the
  303. * {@code VirtualBackgroundPreview} component.
  304. *
  305. * @param {Object} state - The Redux state.
  306. * @private
  307. * @returns {{Props}}
  308. */
  309. function _mapStateToProps(state: IState): Object {
  310. return {
  311. _currentCameraDeviceId: getCurrentCameraDeviceId(state)
  312. };
  313. }
  314. export default translate(connect(_mapStateToProps)(withStyles(styles)(VirtualBackgroundPreview)));