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.

DeviceSelectionDialogBase.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. import PropTypes from 'prop-types';
  2. import React, { Component } from 'react';
  3. import { StatelessDialog } from '../../base/dialog';
  4. import { translate } from '../../base/i18n';
  5. import { createLocalTrack } from '../../base/lib-jitsi-meet';
  6. import { shouldShowOnlyDeviceSelection } from '../../settings';
  7. import AudioInputPreview from './AudioInputPreview';
  8. import AudioOutputPreview from './AudioOutputPreview';
  9. import DeviceSelector from './DeviceSelector';
  10. import VideoInputPreview from './VideoInputPreview';
  11. /**
  12. * React component for previewing and selecting new audio and video sources.
  13. *
  14. * @extends Component
  15. */
  16. class DeviceSelectionDialogBase extends Component {
  17. /**
  18. * DeviceSelectionDialogBase component's property types.
  19. *
  20. * @static
  21. */
  22. static propTypes = {
  23. /**
  24. * All known audio and video devices split by type. This prop comes from
  25. * the app state.
  26. */
  27. availableDevices: PropTypes.object,
  28. /**
  29. * Closes the dialog.
  30. */
  31. closeModal: PropTypes.func,
  32. /**
  33. * Device id for the current audio input device. This device will be set
  34. * as the default audio input device to preview.
  35. */
  36. currentAudioInputId: PropTypes.string,
  37. /**
  38. * Device id for the current audio output device. This device will be
  39. * set as the default audio output device to preview.
  40. */
  41. currentAudioOutputId: PropTypes.string,
  42. /**
  43. * Device id for the current video input device. This device will be set
  44. * as the default video input device to preview.
  45. */
  46. currentVideoInputId: PropTypes.string,
  47. /**
  48. * Whether or not the audio selector can be interacted with. If true,
  49. * the audio input selector will be rendered as disabled. This is
  50. * specifically used to prevent audio device changing in Firefox, which
  51. * currently does not work due to a browser-side regression.
  52. */
  53. disableAudioInputChange: PropTypes.bool,
  54. /**
  55. * Disables dismissing the dialog when the blanket is clicked. Enabled
  56. * by default.
  57. */
  58. disableBlanketClickDismiss: PropTypes.bool,
  59. /**
  60. * True if device changing is configured to be disallowed. Selectors
  61. * will display as disabled.
  62. */
  63. disableDeviceChange: PropTypes.bool,
  64. /**
  65. * Function that checks whether or not a new audio input source can be
  66. * selected.
  67. */
  68. hasAudioPermission: PropTypes.func,
  69. /**
  70. * Function that checks whether or not a new video input sources can be
  71. * selected.
  72. */
  73. hasVideoPermission: PropTypes.func,
  74. /**
  75. * If true, the audio meter will not display. Necessary for browsers or
  76. * configurations that do not support local stats to prevent a
  77. * non-responsive mic preview from displaying.
  78. */
  79. hideAudioInputPreview: PropTypes.bool,
  80. /**
  81. * Whether or not the audio output source selector should display. If
  82. * true, the audio output selector and test audio link will not be
  83. * rendered. This is specifically used for hiding audio output on
  84. * temasys browsers which do not support such change.
  85. */
  86. hideAudioOutputSelect: PropTypes.bool,
  87. /**
  88. * Function that sets the audio input device.
  89. */
  90. setAudioInputDevice: PropTypes.func,
  91. /**
  92. * Function that sets the audio output device.
  93. */
  94. setAudioOutputDevice: PropTypes.func,
  95. /**
  96. * Function that sets the video input device.
  97. */
  98. setVideoInputDevice: PropTypes.func,
  99. /**
  100. * Invoked to obtain translated strings.
  101. */
  102. t: PropTypes.func
  103. };
  104. /**
  105. * Initializes a new DeviceSelectionDialogBase instance.
  106. *
  107. * @param {Object} props - The read-only React Component props with which
  108. * the new instance is to be initialized.
  109. */
  110. constructor(props) {
  111. super(props);
  112. const { availableDevices } = this.props;
  113. this.state = {
  114. // JitsiLocalTrack to use for live previewing of audio input.
  115. previewAudioTrack: null,
  116. // JitsiLocalTrack to use for live previewing of video input.
  117. previewVideoTrack: null,
  118. // An message describing a problem with obtaining a video preview.
  119. previewVideoTrackError: null,
  120. // The audio input device id to show as selected by default.
  121. selectedAudioInputId: this.props.currentAudioInputId || '',
  122. // The audio output device id to show as selected by default.
  123. selectedAudioOutputId: this.props.currentAudioOutputId || '',
  124. // The video input device id to show as selected by default.
  125. // FIXME: On temasys, without a device selected and put into local
  126. // storage as the default device to use, the current video device id
  127. // is a blank string. This is because the library gets a local video
  128. // track and then maps the track's device id by matching the track's
  129. // label to the MediaDeviceInfos returned from enumerateDevices. In
  130. // WebRTC, the track label is expected to return the camera device
  131. // label. However, temasys video track labels refer to track id, not
  132. // device label, so the library cannot match the track to a device.
  133. // The workaround of defaulting to the first videoInput available
  134. // is re-used from the previous device settings implementation.
  135. selectedVideoInputId: this.props.currentVideoInputId
  136. || (availableDevices.videoInput
  137. && availableDevices.videoInput[0]
  138. && availableDevices.videoInput[0].deviceId)
  139. || ''
  140. };
  141. // Preventing closing while cleaning up previews is important for
  142. // supporting temasys video cleanup. Temasys requires its video object
  143. // to be in the dom and visible for proper detaching of tracks. Delaying
  144. // closure until cleanup is complete ensures no errors in the process.
  145. this._isClosing = false;
  146. this._setDevicesAndClose = this._setDevicesAndClose.bind(this);
  147. this._onCancel = this._onCancel.bind(this);
  148. this._onSubmit = this._onSubmit.bind(this);
  149. this._updateAudioOutput = this._updateAudioOutput.bind(this);
  150. this._updateAudioInput = this._updateAudioInput.bind(this);
  151. this._updateVideoInput = this._updateVideoInput.bind(this);
  152. }
  153. /**
  154. * Sets default device choices so a choice is pre-selected in the dropdowns
  155. * and live previews are created.
  156. *
  157. * @inheritdoc
  158. */
  159. componentDidMount() {
  160. this._updateAudioOutput(this.state.selectedAudioOutputId);
  161. this._updateAudioInput(this.state.selectedAudioInputId);
  162. this._updateVideoInput(this.state.selectedVideoInputId);
  163. }
  164. /**
  165. * Disposes preview tracks that might not already be disposed.
  166. *
  167. * @inheritdoc
  168. */
  169. componentWillUnmount() {
  170. // This handles the case where neither submit nor cancel were triggered,
  171. // such as on modal switch. In that case, make a dying attempt to clean
  172. // up previews.
  173. if (!this._isClosing) {
  174. this._attemptPreviewTrackCleanup();
  175. }
  176. }
  177. /**
  178. * Implements React's {@link Component#render()}.
  179. *
  180. * @inheritdoc
  181. */
  182. render() {
  183. return (
  184. <StatelessDialog
  185. cancelTitleKey = { 'dialog.Cancel' }
  186. disableBlanketClickDismiss
  187. = { this.props.disableBlanketClickDismiss }
  188. okTitleKey = { 'dialog.Save' }
  189. onCancel = { this._onCancel }
  190. onSubmit = { this._onSubmit }
  191. titleKey = { this._getModalTitle() }>
  192. <div className = 'device-selection'>
  193. <div className = 'device-selection-column column-video'>
  194. <div className = 'device-selection-video-container'>
  195. <VideoInputPreview
  196. error = { this.state.previewVideoTrackError }
  197. track = { this.state.previewVideoTrack } />
  198. </div>
  199. { this._renderAudioInputPreview() }
  200. </div>
  201. <div className = 'device-selection-column column-selectors'>
  202. <div className = 'device-selectors'>
  203. { this._renderSelectors() }
  204. </div>
  205. { this._renderAudioOutputPreview() }
  206. </div>
  207. </div>
  208. </StatelessDialog>
  209. );
  210. }
  211. /**
  212. * Cleans up preview tracks if they are not active tracks.
  213. *
  214. * @private
  215. * @returns {Array<Promise>} Zero to two promises will be returned. One
  216. * promise can be for video cleanup and another for audio cleanup.
  217. */
  218. _attemptPreviewTrackCleanup() {
  219. return Promise.all([
  220. this._disposeVideoPreview(),
  221. this._disposeAudioPreview()
  222. ]);
  223. }
  224. /**
  225. * Utility function for disposing the current audio preview.
  226. *
  227. * @private
  228. * @returns {Promise}
  229. */
  230. _disposeAudioPreview() {
  231. return this.state.previewAudioTrack
  232. ? this.state.previewAudioTrack.dispose() : Promise.resolve();
  233. }
  234. /**
  235. * Utility function for disposing the current video preview.
  236. *
  237. * @private
  238. * @returns {Promise}
  239. */
  240. _disposeVideoPreview() {
  241. return this.state.previewVideoTrack
  242. ? this.state.previewVideoTrack.dispose() : Promise.resolve();
  243. }
  244. /**
  245. * Returns what the title of the device selection modal should be.
  246. *
  247. * Note: This is temporary logic to appease design sooner. Device selection
  248. * and all other settings will be combined into one modal.
  249. *
  250. * @returns {string}
  251. */
  252. _getModalTitle() {
  253. if (shouldShowOnlyDeviceSelection()) {
  254. return 'settings.title';
  255. }
  256. return 'deviceSelection.deviceSettings';
  257. }
  258. /**
  259. * Disposes preview tracks and signals to
  260. * close DeviceSelectionDialogBase.
  261. *
  262. * @private
  263. * @returns {boolean} Returns false to prevent closure until cleanup is
  264. * complete.
  265. */
  266. _onCancel() {
  267. if (this._isClosing) {
  268. return false;
  269. }
  270. this._isClosing = true;
  271. const cleanupPromises = this._attemptPreviewTrackCleanup();
  272. Promise.all(cleanupPromises)
  273. .then(this.props.closeModal)
  274. .catch(this.props.closeModal);
  275. return false;
  276. }
  277. /**
  278. * Identifies changes to the preferred input/output devices and perform
  279. * necessary cleanup and requests to use those devices. Closes the modal
  280. * after cleanup and device change requests complete.
  281. *
  282. * @private
  283. * @returns {boolean} Returns false to prevent closure until cleanup is
  284. * complete.
  285. */
  286. _onSubmit() {
  287. if (this._isClosing) {
  288. return false;
  289. }
  290. this._isClosing = true;
  291. this._attemptPreviewTrackCleanup()
  292. .then(this._setDevicesAndClose, this._setDevicesAndClose);
  293. return false;
  294. }
  295. /**
  296. * Creates an AudioInputPreview for previewing if audio is being received.
  297. * Null will be returned if local stats for tracking audio input levels
  298. * cannot be obtained.
  299. *
  300. * @private
  301. * @returns {ReactComponent|null}
  302. */
  303. _renderAudioInputPreview() {
  304. if (this.props.hideAudioInputPreview) {
  305. return null;
  306. }
  307. return (
  308. <AudioInputPreview
  309. track = { this.state.previewAudioTrack } />
  310. );
  311. }
  312. /**
  313. * Creates an AudioOutputPreview instance for playing a test sound with the
  314. * passed in device id. Null will be returned if hideAudioOutput is truthy.
  315. *
  316. * @private
  317. * @returns {ReactComponent|null}
  318. */
  319. _renderAudioOutputPreview() {
  320. if (this.props.hideAudioOutputSelect) {
  321. return null;
  322. }
  323. return (
  324. <AudioOutputPreview
  325. deviceId = { this.state.selectedAudioOutputId } />
  326. );
  327. }
  328. /**
  329. * Creates a DeviceSelector instance based on the passed in configuration.
  330. *
  331. * @private
  332. * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
  333. * @returns {ReactElement}
  334. */
  335. _renderSelector(deviceSelectorProps) {
  336. return (
  337. <div key = { deviceSelectorProps.label }>
  338. <div className = 'device-selector-label'>
  339. { this.props.t(deviceSelectorProps.label) }
  340. </div>
  341. <DeviceSelector { ...deviceSelectorProps } />
  342. </div>
  343. );
  344. }
  345. /**
  346. * Creates DeviceSelector instances for video output, audio input, and audio
  347. * output.
  348. *
  349. * @private
  350. * @returns {Array<ReactElement>} DeviceSelector instances.
  351. */
  352. _renderSelectors() {
  353. const { availableDevices } = this.props;
  354. const configurations = [
  355. {
  356. devices: availableDevices.videoInput,
  357. hasPermission: this.props.hasVideoPermission(),
  358. icon: 'icon-camera',
  359. isDisabled: this.props.disableDeviceChange,
  360. key: 'videoInput',
  361. label: 'settings.selectCamera',
  362. onSelect: this._updateVideoInput,
  363. selectedDeviceId: this.state.selectedVideoInputId
  364. },
  365. {
  366. devices: availableDevices.audioInput,
  367. hasPermission: this.props.hasAudioPermission(),
  368. icon: 'icon-microphone',
  369. isDisabled: this.props.disableAudioInputChange
  370. || this.props.disableDeviceChange,
  371. key: 'audioInput',
  372. label: 'settings.selectMic',
  373. onSelect: this._updateAudioInput,
  374. selectedDeviceId: this.state.selectedAudioInputId
  375. }
  376. ];
  377. if (!this.props.hideAudioOutputSelect) {
  378. configurations.push({
  379. devices: availableDevices.audioOutput,
  380. hasPermission: this.props.hasAudioPermission()
  381. || this.props.hasVideoPermission(),
  382. icon: 'icon-volume',
  383. isDisabled: this.props.disableDeviceChange,
  384. key: 'audioOutput',
  385. label: 'settings.selectAudioOutput',
  386. onSelect: this._updateAudioOutput,
  387. selectedDeviceId: this.state.selectedAudioOutputId
  388. });
  389. }
  390. return configurations.map(config => this._renderSelector(config));
  391. }
  392. /**
  393. * Sets the selected devices and closes the dialog.
  394. *
  395. * @returns {void}
  396. */
  397. _setDevicesAndClose() {
  398. const {
  399. setVideoInputDevice,
  400. setAudioInputDevice,
  401. setAudioOutputDevice,
  402. closeModal
  403. } = this.props;
  404. const promises = [];
  405. if (this.state.selectedVideoInputId
  406. !== this.props.currentVideoInputId) {
  407. promises.push(setVideoInputDevice(this.state.selectedVideoInputId));
  408. }
  409. if (this.state.selectedAudioInputId
  410. !== this.props.currentAudioInputId) {
  411. promises.push(setAudioInputDevice(this.state.selectedAudioInputId));
  412. }
  413. if (this.state.selectedAudioOutputId
  414. !== this.props.currentAudioOutputId) {
  415. promises.push(
  416. setAudioOutputDevice(this.state.selectedAudioOutputId));
  417. }
  418. Promise.all(promises).then(closeModal, closeModal);
  419. }
  420. /**
  421. * Callback invoked when a new audio input device has been selected. Updates
  422. * the internal state of the user's selection as well as the audio track
  423. * that should display in the preview.
  424. *
  425. * @param {string} deviceId - The id of the chosen audio input device.
  426. * @private
  427. * @returns {void}
  428. */
  429. _updateAudioInput(deviceId) {
  430. this.setState({
  431. selectedAudioInputId: deviceId
  432. }, () => {
  433. this._disposeAudioPreview()
  434. .then(() => createLocalTrack('audio', deviceId))
  435. .then(jitsiLocalTrack => {
  436. this.setState({
  437. previewAudioTrack: jitsiLocalTrack
  438. });
  439. })
  440. .catch(() => {
  441. this.setState({
  442. previewAudioTrack: null
  443. });
  444. });
  445. });
  446. }
  447. /**
  448. * Callback invoked when a new audio output device has been selected.
  449. * Updates the internal state of the user's selection.
  450. *
  451. * @param {string} deviceId - The id of the chosen audio output device.
  452. * @private
  453. * @returns {void}
  454. */
  455. _updateAudioOutput(deviceId) {
  456. this.setState({
  457. selectedAudioOutputId: deviceId
  458. });
  459. }
  460. /**
  461. * Callback invoked when a new video input device has been selected. Updates
  462. * the internal state of the user's selection as well as the video track
  463. * that should display in the preview.
  464. *
  465. * @param {string} deviceId - The id of the chosen video input device.
  466. * @private
  467. * @returns {void}
  468. */
  469. _updateVideoInput(deviceId) {
  470. this.setState({
  471. selectedVideoInputId: deviceId
  472. }, () => {
  473. this._disposeVideoPreview()
  474. .then(() => createLocalTrack('video', deviceId))
  475. .then(jitsiLocalTrack => {
  476. if (!jitsiLocalTrack) {
  477. return Promise.reject();
  478. }
  479. this.setState({
  480. previewVideoTrack: jitsiLocalTrack,
  481. previewVideoTrackError: null
  482. });
  483. })
  484. .catch(() => {
  485. this.setState({
  486. previewVideoTrack: null,
  487. previewVideoTrackError:
  488. this.props.t('deviceSelection.previewUnavailable')
  489. });
  490. });
  491. });
  492. }
  493. }
  494. export default translate(DeviceSelectionDialogBase);