Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

VideoDeviceSelection.web.tsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import { Theme } from '@mui/material';
  2. import React from 'react';
  3. import { WithTranslation } from 'react-i18next';
  4. import { connect } from 'react-redux';
  5. import { withStyles } from 'tss-react/mui';
  6. import { IReduxState, IStore } from '../../app/types';
  7. import { getAvailableDevices } from '../../base/devices/actions.web';
  8. import AbstractDialogTab, {
  9. type IProps as AbstractDialogTabProps
  10. } from '../../base/dialog/components/web/AbstractDialogTab';
  11. import { translate } from '../../base/i18n/functions';
  12. import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
  13. import Checkbox from '../../base/ui/components/web/Checkbox';
  14. import Select from '../../base/ui/components/web/Select';
  15. import { SS_DEFAULT_FRAME_RATE } from '../../settings/constants';
  16. import logger from '../logger';
  17. import DeviceSelector from './DeviceSelector.web';
  18. import VideoInputPreview from './VideoInputPreview';
  19. /**
  20. * The type of the React {@code Component} props of {@link VideoDeviceSelection}.
  21. */
  22. export interface IProps extends AbstractDialogTabProps, WithTranslation {
  23. /**
  24. * All known audio and video devices split by type. This prop comes from
  25. * the app state.
  26. */
  27. availableDevices: { videoInput?: MediaDeviceInfo[]; };
  28. /**
  29. * CSS classes object.
  30. */
  31. classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
  32. /**
  33. * The currently selected desktop share frame rate in the frame rate select dropdown.
  34. */
  35. currentFramerate: string;
  36. /**
  37. * All available desktop capture frame rates.
  38. */
  39. desktopShareFramerates: Array<number>;
  40. /**
  41. * True if desktop share settings should be hidden (mobile browsers).
  42. */
  43. disableDesktopShareSettings: boolean;
  44. /**
  45. * True if device changing is configured to be disallowed. Selectors
  46. * will display as disabled.
  47. */
  48. disableDeviceChange: boolean;
  49. /**
  50. * Whether the local video can be flipped or not.
  51. */
  52. disableLocalVideoFlip: boolean | undefined;
  53. /**
  54. * Whether video input dropdown should be enabled or not.
  55. */
  56. disableVideoInputSelect: boolean;
  57. /**
  58. * Redux dispatch.
  59. */
  60. dispatch: IStore['dispatch'];
  61. /**
  62. * Whether or not the audio permission was granted.
  63. */
  64. hasVideoPermission: boolean;
  65. /**
  66. * Whether to hide the additional settings or not.
  67. */
  68. hideAdditionalSettings: boolean;
  69. /**
  70. * Whether video input preview should be displayed or not.
  71. * (In the case of iOS Safari).
  72. */
  73. hideVideoInputPreview: boolean;
  74. /**
  75. * Whether or not the local video is flipped.
  76. */
  77. localFlipX: boolean;
  78. /**
  79. * The id of the video input device to preview.
  80. */
  81. selectedVideoInputId: string;
  82. }
  83. /**
  84. * The type of the React {@code Component} state of {@link VideoDeviceSelection}.
  85. */
  86. interface IState {
  87. /**
  88. * The JitsiTrack to use for previewing video input.
  89. */
  90. previewVideoTrack: any | null;
  91. /**
  92. * The error message from trying to use a video input device.
  93. */
  94. previewVideoTrackError: string | null;
  95. }
  96. const styles = (theme: Theme) => {
  97. return {
  98. container: {
  99. display: 'flex',
  100. flexDirection: 'column' as const,
  101. padding: '0 2px',
  102. width: '100%'
  103. },
  104. checkboxContainer: {
  105. margin: `${theme.spacing(4)} 0`
  106. }
  107. };
  108. };
  109. /**
  110. * React {@code Component} for previewing audio and video input/output devices.
  111. *
  112. * @augments Component
  113. */
  114. class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
  115. /**
  116. * Whether current component is mounted or not.
  117. *
  118. * In component did mount we start a Promise to create tracks and
  119. * set the tracks in the state, if we unmount the component in the meanwhile
  120. * tracks will be created and will never been disposed (dispose tracks is
  121. * in componentWillUnmount). When tracks are created and component is
  122. * unmounted we dispose the tracks.
  123. */
  124. _unMounted: boolean;
  125. /**
  126. * Initializes a new DeviceSelection instance.
  127. *
  128. * @param {Object} props - The read-only React Component props with which
  129. * the new instance is to be initialized.
  130. */
  131. constructor(props: IProps) {
  132. super(props);
  133. this.state = {
  134. previewVideoTrack: null,
  135. previewVideoTrackError: null
  136. };
  137. this._unMounted = true;
  138. this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
  139. }
  140. /**
  141. * Generate the initial previews for audio input and video input.
  142. *
  143. * @inheritdoc
  144. */
  145. componentDidMount() {
  146. this._unMounted = false;
  147. Promise.all([
  148. this._createVideoInputTrack(this.props.selectedVideoInputId)
  149. ])
  150. .catch(err => logger.warn('Failed to initialize preview tracks', err))
  151. .then(() => {
  152. this.props.dispatch(getAvailableDevices());
  153. });
  154. }
  155. /**
  156. * Checks if audio / video permissions were granted. Updates audio input and
  157. * video input previews.
  158. *
  159. * @param {Object} prevProps - Previous props this component received.
  160. * @returns {void}
  161. */
  162. componentDidUpdate(prevProps: IProps) {
  163. if (prevProps.selectedVideoInputId
  164. !== this.props.selectedVideoInputId) {
  165. this._createVideoInputTrack(this.props.selectedVideoInputId);
  166. }
  167. }
  168. /**
  169. * Ensure preview tracks are destroyed to prevent continued use.
  170. *
  171. * @inheritdoc
  172. */
  173. componentWillUnmount() {
  174. this._unMounted = true;
  175. this._disposeVideoInputPreview();
  176. }
  177. /**
  178. * Implements React's {@link Component#render()}.
  179. *
  180. * @inheritdoc
  181. */
  182. render() {
  183. const {
  184. disableDesktopShareSettings,
  185. disableLocalVideoFlip,
  186. hideAdditionalSettings,
  187. hideVideoInputPreview,
  188. localFlipX,
  189. t
  190. } = this.props;
  191. const classes = withStyles.getClasses(this.props);
  192. return (
  193. <div className = { classes.container }>
  194. { !hideVideoInputPreview
  195. && <VideoInputPreview
  196. error = { this.state.previewVideoTrackError }
  197. localFlipX = { localFlipX }
  198. track = { this.state.previewVideoTrack } />
  199. }
  200. <div
  201. aria-live = 'polite'>
  202. {this._renderVideoSelector()}
  203. </div>
  204. {!hideAdditionalSettings && (
  205. <>
  206. {!disableLocalVideoFlip && (
  207. <div className = { classes.checkboxContainer }>
  208. <Checkbox
  209. checked = { localFlipX }
  210. label = { t('videothumbnail.mirrorVideo') }
  211. // eslint-disable-next-line react/jsx-no-bind
  212. onChange = { () => super._onChange({ localFlipX: !localFlipX }) } />
  213. </div>
  214. )}
  215. {!disableDesktopShareSettings && this._renderFramerateSelect()}
  216. </>
  217. )}
  218. </div>
  219. );
  220. }
  221. /**
  222. * Creates the JitsiTrack for the video input preview.
  223. *
  224. * @param {string} deviceId - The id of video device to preview.
  225. * @private
  226. * @returns {void}
  227. */
  228. _createVideoInputTrack(deviceId: string) {
  229. const { hideVideoInputPreview } = this.props;
  230. if (hideVideoInputPreview) {
  231. return;
  232. }
  233. return this._disposeVideoInputPreview()
  234. .then(() => createLocalTrack('video', deviceId, 5000))
  235. .then(jitsiLocalTrack => {
  236. if (!jitsiLocalTrack) {
  237. return Promise.reject();
  238. }
  239. if (this._unMounted) {
  240. jitsiLocalTrack.dispose();
  241. return;
  242. }
  243. this.setState({
  244. previewVideoTrack: jitsiLocalTrack,
  245. previewVideoTrackError: null
  246. });
  247. })
  248. .catch(() => {
  249. this.setState({
  250. previewVideoTrack: null,
  251. previewVideoTrackError:
  252. this.props.t('deviceSelection.previewUnavailable')
  253. });
  254. });
  255. }
  256. /**
  257. * Utility function for disposing the current video input preview.
  258. *
  259. * @private
  260. * @returns {Promise}
  261. */
  262. _disposeVideoInputPreview(): Promise<any> {
  263. return this.state.previewVideoTrack
  264. ? this.state.previewVideoTrack.dispose() : Promise.resolve();
  265. }
  266. /**
  267. * Creates a DeviceSelector instance based on the passed in configuration.
  268. *
  269. * @private
  270. * @returns {ReactElement}
  271. */
  272. _renderVideoSelector() {
  273. const { availableDevices, hasVideoPermission } = this.props;
  274. const videoConfig = {
  275. devices: availableDevices.videoInput,
  276. hasPermission: hasVideoPermission,
  277. icon: 'icon-camera',
  278. isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
  279. key: 'videoInput',
  280. id: 'videoInput',
  281. label: 'settings.selectCamera',
  282. onSelect: (selectedVideoInputId: string) => super._onChange({ selectedVideoInputId }),
  283. selectedDeviceId: this.state.previewVideoTrack
  284. ? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
  285. };
  286. return (
  287. <DeviceSelector
  288. { ...videoConfig }
  289. key = { videoConfig.id } />
  290. );
  291. }
  292. /**
  293. * Callback invoked to select a frame rate from the select dropdown.
  294. *
  295. * @param {Object} e - The key event to handle.
  296. * @private
  297. * @returns {void}
  298. */
  299. _onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
  300. const frameRate = e.target.value;
  301. super._onChange({ currentFramerate: frameRate });
  302. }
  303. /**
  304. * Returns the React Element for the desktop share frame rate dropdown.
  305. *
  306. * @returns {JSX}
  307. */
  308. _renderFramerateSelect() {
  309. const { currentFramerate, desktopShareFramerates, t } = this.props;
  310. const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
  311. return {
  312. value: frameRate,
  313. label: `${frameRate} ${t('settings.framesPerSecond')}`
  314. };
  315. });
  316. return (
  317. <Select
  318. bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
  319. ? t('settings.desktopShareHighFpsWarning')
  320. : t('settings.desktopShareWarning') }
  321. id = 'more-framerate-select'
  322. label = { t('settings.desktopShareFramerate') }
  323. onChange = { this._onFramerateItemSelect }
  324. options = { frameRateItems }
  325. value = { currentFramerate } />
  326. );
  327. }
  328. }
  329. const mapStateToProps = (state: IReduxState) => {
  330. return {
  331. availableDevices: state['features/base/devices'].availableDevices ?? {}
  332. };
  333. };
  334. export default connect(mapStateToProps)(withStyles(translate(VideoDeviceSelection), styles));