您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

VideoDeviceSelection.web.tsx 11KB

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