Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

DeviceSelection.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. // @flow
  2. import React from 'react';
  3. import { getAvailableDevices } from '../../base/devices/actions.web';
  4. import AbstractDialogTab, {
  5. type Props as AbstractDialogTabProps
  6. } from '../../base/dialog/components/web/AbstractDialogTab';
  7. import { translate } from '../../base/i18n/functions';
  8. import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
  9. import logger from '../logger';
  10. import AudioInputPreview from './AudioInputPreview';
  11. import AudioOutputPreview from './AudioOutputPreview';
  12. import DeviceHidContainer from './DeviceHidContainer.web';
  13. import DeviceSelector from './DeviceSelector';
  14. import VideoInputPreview from './VideoInputPreview';
  15. /**
  16. * The type of the React {@code Component} props of {@link DeviceSelection}.
  17. */
  18. export type Props = {
  19. ...$Exact<AbstractDialogTabProps>,
  20. /**
  21. * All known audio and video devices split by type. This prop comes from
  22. * the app state.
  23. */
  24. availableDevices: Object,
  25. /**
  26. * Whether or not the audio selector can be interacted with. If true,
  27. * the audio input selector will be rendered as disabled. This is
  28. * specifically used to prevent audio device changing in Firefox, which
  29. * currently does not work due to a browser-side regression.
  30. */
  31. disableAudioInputChange: boolean,
  32. /**
  33. * True if device changing is configured to be disallowed. Selectors
  34. * will display as disabled.
  35. */
  36. disableDeviceChange: boolean,
  37. /**
  38. * Whether video input dropdown should be enabled or not.
  39. */
  40. disableVideoInputSelect: boolean,
  41. /**
  42. * Whether or not the audio permission was granted.
  43. */
  44. hasAudioPermission: boolean,
  45. /**
  46. * Whether or not the audio permission was granted.
  47. */
  48. hasVideoPermission: boolean,
  49. /**
  50. * If true, the audio meter will not display. Necessary for browsers or
  51. * configurations that do not support local stats to prevent a
  52. * non-responsive mic preview from displaying.
  53. */
  54. hideAudioInputPreview: boolean,
  55. /**
  56. * If true, the button to play a test sound on the selected speaker will not be displayed.
  57. * This needs to be hidden on browsers that do not support selecting an audio output device.
  58. */
  59. hideAudioOutputPreview: boolean,
  60. /**
  61. * Whether or not the audio output source selector should display. If
  62. * true, the audio output selector and test audio link will not be
  63. * rendered.
  64. */
  65. hideAudioOutputSelect: boolean,
  66. /**
  67. * Whether or not the hid device container should display.
  68. */
  69. hideDeviceHIDContainer: boolean,
  70. /**
  71. * Whether video input preview should be displayed or not.
  72. * (In the case of iOS Safari).
  73. */
  74. hideVideoInputPreview: boolean,
  75. /**
  76. * The id of the audio input device to preview.
  77. */
  78. selectedAudioInputId: string,
  79. /**
  80. * The id of the audio output device to preview.
  81. */
  82. selectedAudioOutputId: string,
  83. /**
  84. * The id of the video input device to preview.
  85. */
  86. selectedVideoInputId: string,
  87. /**
  88. * Invoked to obtain translated strings.
  89. */
  90. t: Function
  91. };
  92. /**
  93. * The type of the React {@code Component} state of {@link DeviceSelection}.
  94. */
  95. type State = {
  96. /**
  97. * The JitsiTrack to use for previewing audio input.
  98. */
  99. previewAudioTrack: ?Object,
  100. /**
  101. * The JitsiTrack to use for previewing video input.
  102. */
  103. previewVideoTrack: ?Object,
  104. /**
  105. * The error message from trying to use a video input device.
  106. */
  107. previewVideoTrackError: ?string
  108. };
  109. /**
  110. * React {@code Component} for previewing audio and video input/output devices.
  111. *
  112. * @augments Component
  113. */
  114. class DeviceSelection extends AbstractDialogTab<Props, State> {
  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: Props) {
  132. super(props);
  133. this.state = {
  134. previewAudioTrack: null,
  135. previewVideoTrack: null,
  136. previewVideoTrackError: null
  137. };
  138. this._unMounted = true;
  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._createAudioInputTrack(this.props.selectedAudioInputId),
  149. this._createVideoInputTrack(this.props.selectedVideoInputId)
  150. ])
  151. .catch(err => logger.warn('Failed to initialize preview tracks', err))
  152. .then(() => getAvailableDevices());
  153. }
  154. /**
  155. * Checks if audio / video permissions were granted. Updates audio input and
  156. * video input previews.
  157. *
  158. * @param {Object} prevProps - Previous props this component received.
  159. * @returns {void}
  160. */
  161. componentDidUpdate(prevProps) {
  162. if (prevProps.selectedAudioInputId
  163. !== this.props.selectedAudioInputId) {
  164. this._createAudioInputTrack(this.props.selectedAudioInputId);
  165. }
  166. if (prevProps.selectedVideoInputId
  167. !== this.props.selectedVideoInputId) {
  168. this._createVideoInputTrack(this.props.selectedVideoInputId);
  169. }
  170. }
  171. /**
  172. * Ensure preview tracks are destroyed to prevent continued use.
  173. *
  174. * @inheritdoc
  175. */
  176. componentWillUnmount() {
  177. this._unMounted = true;
  178. this._disposeAudioInputPreview();
  179. this._disposeVideoInputPreview();
  180. }
  181. /**
  182. * Implements React's {@link Component#render()}.
  183. *
  184. * @inheritdoc
  185. */
  186. render() {
  187. const {
  188. hideAudioInputPreview,
  189. hideAudioOutputPreview,
  190. hideDeviceHIDContainer,
  191. hideVideoInputPreview,
  192. selectedAudioOutputId
  193. } = this.props;
  194. return (
  195. <div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
  196. <div className = 'device-selection-column column-video'>
  197. { !hideVideoInputPreview
  198. && <div className = 'device-selection-video-container'>
  199. <VideoInputPreview
  200. error = { this.state.previewVideoTrackError }
  201. track = { this.state.previewVideoTrack } />
  202. </div>
  203. }
  204. { !hideAudioInputPreview
  205. && <AudioInputPreview
  206. track = { this.state.previewAudioTrack } /> }
  207. </div>
  208. <div className = 'device-selection-column column-selectors'>
  209. <div
  210. aria-live = 'polite all'
  211. className = 'device-selectors'>
  212. { this._renderSelectors() }
  213. </div>
  214. { !hideAudioOutputPreview
  215. && <AudioOutputPreview
  216. deviceId = { selectedAudioOutputId } /> }
  217. { !hideDeviceHIDContainer
  218. && <DeviceHidContainer /> }
  219. </div>
  220. </div>
  221. );
  222. }
  223. /**
  224. * Creates the JitiTrack for the audio input preview.
  225. *
  226. * @param {string} deviceId - The id of audio input device to preview.
  227. * @private
  228. * @returns {void}
  229. */
  230. _createAudioInputTrack(deviceId) {
  231. const { hideAudioInputPreview } = this.props;
  232. if (hideAudioInputPreview) {
  233. return;
  234. }
  235. return this._disposeAudioInputPreview()
  236. .then(() => createLocalTrack('audio', deviceId, 5000))
  237. .then(jitsiLocalTrack => {
  238. if (this._unMounted) {
  239. jitsiLocalTrack.dispose();
  240. return;
  241. }
  242. this.setState({
  243. previewAudioTrack: jitsiLocalTrack
  244. });
  245. })
  246. .catch(() => {
  247. this.setState({
  248. previewAudioTrack: null
  249. });
  250. });
  251. }
  252. /**
  253. * Creates the JitiTrack for the video input preview.
  254. *
  255. * @param {string} deviceId - The id of video device to preview.
  256. * @private
  257. * @returns {void}
  258. */
  259. _createVideoInputTrack(deviceId) {
  260. const { hideVideoInputPreview } = this.props;
  261. if (hideVideoInputPreview) {
  262. return;
  263. }
  264. return this._disposeVideoInputPreview()
  265. .then(() => createLocalTrack('video', deviceId, 5000))
  266. .then(jitsiLocalTrack => {
  267. if (!jitsiLocalTrack) {
  268. return Promise.reject();
  269. }
  270. if (this._unMounted) {
  271. jitsiLocalTrack.dispose();
  272. return;
  273. }
  274. this.setState({
  275. previewVideoTrack: jitsiLocalTrack,
  276. previewVideoTrackError: null
  277. });
  278. })
  279. .catch(() => {
  280. this.setState({
  281. previewVideoTrack: null,
  282. previewVideoTrackError:
  283. this.props.t('deviceSelection.previewUnavailable')
  284. });
  285. });
  286. }
  287. /**
  288. * Utility function for disposing the current audio input preview.
  289. *
  290. * @private
  291. * @returns {Promise}
  292. */
  293. _disposeAudioInputPreview(): Promise<*> {
  294. return this.state.previewAudioTrack
  295. ? this.state.previewAudioTrack.dispose() : Promise.resolve();
  296. }
  297. /**
  298. * Utility function for disposing the current video input preview.
  299. *
  300. * @private
  301. * @returns {Promise}
  302. */
  303. _disposeVideoInputPreview(): Promise<*> {
  304. return this.state.previewVideoTrack
  305. ? this.state.previewVideoTrack.dispose() : Promise.resolve();
  306. }
  307. /**
  308. * Creates a DeviceSelector instance based on the passed in configuration.
  309. *
  310. * @private
  311. * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
  312. * @returns {ReactElement}
  313. */
  314. _renderSelector(deviceSelectorProps) {
  315. return (
  316. <div key = { deviceSelectorProps.label }>
  317. <label
  318. className = 'device-selector-label'
  319. htmlFor = { deviceSelectorProps.id }>
  320. { this.props.t(deviceSelectorProps.label) }
  321. </label>
  322. <DeviceSelector { ...deviceSelectorProps } />
  323. </div>
  324. );
  325. }
  326. /**
  327. * Creates DeviceSelector instances for video output, audio input, and audio
  328. * output.
  329. *
  330. * @private
  331. * @returns {Array<ReactElement>} DeviceSelector instances.
  332. */
  333. _renderSelectors() {
  334. const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
  335. const configurations = [
  336. {
  337. devices: availableDevices.audioInput,
  338. hasPermission: hasAudioPermission,
  339. icon: 'icon-microphone',
  340. isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
  341. key: 'audioInput',
  342. id: 'audioInput',
  343. label: 'settings.selectMic',
  344. onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
  345. selectedDeviceId: this.state.previewAudioTrack
  346. ? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
  347. },
  348. {
  349. devices: availableDevices.videoInput,
  350. hasPermission: hasVideoPermission,
  351. icon: 'icon-camera',
  352. isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
  353. key: 'videoInput',
  354. id: 'videoInput',
  355. label: 'settings.selectCamera',
  356. onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
  357. selectedDeviceId: this.state.previewVideoTrack
  358. ? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
  359. }
  360. ];
  361. if (!this.props.hideAudioOutputSelect) {
  362. configurations.push({
  363. devices: availableDevices.audioOutput,
  364. hasPermission: hasAudioPermission || hasVideoPermission,
  365. icon: 'icon-speaker',
  366. isDisabled: this.props.disableDeviceChange,
  367. key: 'audioOutput',
  368. id: 'audioOutput',
  369. label: 'settings.selectAudioOutput',
  370. onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
  371. selectedDeviceId: this.props.selectedAudioOutputId
  372. });
  373. }
  374. return configurations.map(config => this._renderSelector(config));
  375. }
  376. }
  377. export default translate(DeviceSelection);